zidane 8.2.4 → 8.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat.js +3 -3
- package/dist/daemon-cli.js +2 -2
- package/dist/{dispatcher-Dclv6HXn.js → dispatcher-D_9_QFO3.js} +7 -7
- package/dist/{dispatcher-Dclv6HXn.js.map → dispatcher-D_9_QFO3.js.map} +1 -1
- package/dist/{engine-C_U6kDfA.js → engine-BsWBWgTB.js} +2 -2
- package/dist/{engine-C_U6kDfA.js.map → engine-BsWBWgTB.js.map} +1 -1
- package/dist/{executor-BFaR2nfc.js → executor-BSesPPcU.js} +2 -2
- package/dist/{executor-BFaR2nfc.js.map → executor-BSesPPcU.js.map} +1 -1
- package/dist/{extensions-DIbpUj5R.js → extensions-BJof-2mi.js} +2 -2
- package/dist/{extensions-DIbpUj5R.js.map → extensions-BJof-2mi.js.map} +1 -1
- package/dist/extensions.js +1 -1
- package/dist/{host-signals-CQ2jZR-2.js → host-signals-CUKPZZxC.js} +5 -5
- package/dist/{host-signals-CQ2jZR-2.js.map → host-signals-CUKPZZxC.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/ipc/remote.js +3 -3
- package/dist/ipc.js +6 -6
- package/dist/{links-D5itfH8P.js → links-Bto-5dVu.js} +6 -6
- package/dist/{links-D5itfH8P.js.map → links-Bto-5dVu.js.map} +1 -1
- package/dist/{oauth-Ck3wQX4Q.js → oauth-BwPiX0hL.js} +2 -2
- package/dist/{oauth-Ck3wQX4Q.js.map → oauth-BwPiX0hL.js.map} +1 -1
- package/dist/{remote-hosts-BkOwRszu.js → remote-hosts-DcCh7fqp.js} +4 -4
- package/dist/{remote-hosts-BkOwRszu.js.map → remote-hosts-DcCh7fqp.js.map} +1 -1
- package/dist/{settings-keys-QoGJ5n3e.js → settings-keys-CnrJgT1T.js} +2 -2
- package/dist/{settings-keys-QoGJ5n3e.js.map → settings-keys-CnrJgT1T.js.map} +1 -1
- package/dist/{shell-path-BXXtRmEA.js → shell-path-rJDa0OMv.js} +3 -3
- package/dist/{shell-path-BXXtRmEA.js.map → shell-path-rJDa0OMv.js.map} +1 -1
- package/dist/{transcript-anchors-D8jWLpe5.js → transcript-anchors-BcFLh3ZA.js} +3 -3
- package/dist/{transcript-anchors-D8jWLpe5.js.map → transcript-anchors-BcFLh3ZA.js.map} +1 -1
- package/dist/tui.js +8 -8
- package/dist/{version-DDRBX7K6.js → version-DOvIuU8M.js} +2 -2
- package/dist/{version-DDRBX7K6.js.map → version-DOvIuU8M.js.map} +1 -1
- package/dist/{workspace-exec-stream-iHv9cooF.js → workspace-exec-stream-Bzxczzu2.js} +4 -4
- package/dist/{workspace-exec-stream-iHv9cooF.js.map → workspace-exec-stream-Bzxczzu2.js.map} +1 -1
- package/dist/{ws-B2HB87nz.js → ws-CrD-4Mx6.js} +3 -3
- package/dist/{ws-B2HB87nz.js.map → ws-CrD-4Mx6.js.map} +1 -1
- package/dist/{ws-client-DPC9ZJHr.js → ws-client-Dond8vT3.js} +2 -2
- package/dist/{ws-client-DPC9ZJHr.js.map → ws-client-Dond8vT3.js.map} +1 -1
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -25,10 +25,10 @@ import { r as browseSkillSource, s as installSkillFromSpec } from "./skills-DxT9
|
|
|
25
25
|
import { a as groupSkillPicks, c as skillPickOf, n as RECOMMENDED_SKILL_SOURCES, o as skillCategory, s as skillPickKey } from "./weights-DksCExeo.js";
|
|
26
26
|
import { d as ZIDANE_SKILL_EDIT_SKILL_NAME, g as extensionScaffold, u as ZIDANE_EXTENSION_SKILL_NAME } from "./system-freeze-Um9ZJiPo.js";
|
|
27
27
|
import { D as sessionBadgeTarget, E as runBadgeAction, _ as artifactBadgeTarget, a as panelBadgeText, r as normalizePanelContent, v as badgeMenu } from "./panels-B_DfAx68.js";
|
|
28
|
-
import { $ as sanitizeExtensionConfigActionPatch, A as keybindingsPath, D as ensureKeybindingsFile, E as KEYBINDING_KEY_COL_WIDTH, G as registerExtensionHostModules, I as resolveExtensionKeybindings, J as coerceExtensionConfigInput, O as formatBindingForDisplay, Q as resolveExtensionConfigValue, U as parseExtensionScanPathEnv, V as inlineLoadedExtension, W as scanExtensionDirectories, c as removeInstalledExtension, f as disposeExtensionRegistry, g as partitionExtensionCatalog, i as installExtensionFromSpec, j as matchesBinding, k as groupBindings, l as updateInstalledExtension, m as filterRegistryPlugins, n as ExtensionInstallError, nt as createExtensionBus, p as filterExtensionRegistry, t as createExtensionPromptBridge, u as discoverAndBuildExtensionRegistry, w as KEYBINDING_DEFS, x as createExtensionHostRuntimeBridge, y as createExtensionHud } from "./extensions-
|
|
28
|
+
import { $ as sanitizeExtensionConfigActionPatch, A as keybindingsPath, D as ensureKeybindingsFile, E as KEYBINDING_KEY_COL_WIDTH, G as registerExtensionHostModules, I as resolveExtensionKeybindings, J as coerceExtensionConfigInput, O as formatBindingForDisplay, Q as resolveExtensionConfigValue, U as parseExtensionScanPathEnv, V as inlineLoadedExtension, W as scanExtensionDirectories, c as removeInstalledExtension, f as disposeExtensionRegistry, g as partitionExtensionCatalog, i as installExtensionFromSpec, j as matchesBinding, k as groupBindings, l as updateInstalledExtension, m as filterRegistryPlugins, n as ExtensionInstallError, nt as createExtensionBus, p as filterExtensionRegistry, t as createExtensionPromptBridge, u as discoverAndBuildExtensionRegistry, w as KEYBINDING_DEFS, x as createExtensionHostRuntimeBridge, y as createExtensionHud } from "./extensions-BJof-2mi.js";
|
|
29
29
|
import { i as nextLogLevel, n as filterLogRecords, r as formatLogLine } from "./logs-view-DF7ArI6t.js";
|
|
30
30
|
import { a as createRingBufferSink, c as logFileTimestamp, i as createLoggingHooks, n as createFileSink, o as fanoutSink, r as createLogger } from "./logger-RZVCk9Ow.js";
|
|
31
|
-
import { t as ZIDANE_VERSION } from "./version-
|
|
31
|
+
import { t as ZIDANE_VERSION } from "./version-DOvIuU8M.js";
|
|
32
32
|
import { a as DEFAULT_OAUTH_ACCOUNT_LABEL, p as oauthAccountLabel } from "./cred-keys-C0KaSRp7.js";
|
|
33
33
|
import { t as JsonRpcConnectionClosedError } from "./json-rpc-G0rBfEar.js";
|
|
34
34
|
import { $ as buildPlanSystem, A as applyTodosSnapshot, B as todoProgressLabel, F as pickActiveRunId, Q as buildBuildSystem, R as selectActiveTodos, V as useActiveTodos, b as profileComposition, g as builtInPromptOf, h as availableAgents, n as createSessionInStore, r as deleteSessionData, z as setTodosForRun } from "./session-factory-Ct7J9CK1.js";
|
|
@@ -38,7 +38,7 @@ import { E as restoreChainFallbacks, O as toggleChainEntry, T as reorderKeyed, b
|
|
|
38
38
|
import { $ as formatTokenCount, E as SESSION_EVENT_STATUS_STYLE, H as shortSessionId, J as fitRunReceiptCells, M as isMainSession, R as sessionDirLabel, S as parseModelSetting, V as sessionEventMeta, W as truncateTitle, Y as formatCostUsd, Z as formatRunReceiptCells, a as defaultSessionAgent, b as spacesEnabled, d as normalizeSpaces, f as readSessionSpaceId, h as spaceIdForNewSession, i as activeSpaceOf, it as modelDisplayName, m as sessionSpaceFilter, nt as nameFromModelId, o as defaultSessionKnobs, p as resolveSessionSpace, r as SPACE_METADATA_KEY, s as defaultSessionModel, t as DEFAULT_SPACE_ID, v as spaceOfSwitchItem, x as formatModelSetting, y as spaceSwitchItems, z as sessionEventExcerpt } from "./spaces-C_x2bvEl.js";
|
|
39
39
|
import { F as sparseSettingsUpdate, M as decisionsEnabled, O as DEFAULT_SETTINGS, P as settingsFromStored, _ as BUILTIN_THEMES, a as APPROVAL_MODE_OPTIONS, b as resolveTheme, c as SETTINGS_INPUTS, d as clampFps, f as enforceSettingsInvariants, g as toggleHeldOn, h as settingsSectionOf, m as settingKeywords, o as SETTINGS_CATEGORIES, p as requirementMet, s as SETTINGS_CHOICES, u as SETTINGS_TOGGLES, y as resolveChipColor } from "./secret-shapes-HvaYVzwc.js";
|
|
40
40
|
import { F as suggestSafelistEntries, a as settingsFromWire, c as discoverSystemMd, d as userSystemMdPath, h as discoverProjectMcps, i as overlayHostSettings, l as renderSystemMdSeed, n as WINDOW_LOCAL_SETTINGS, o as settingsToWire, r as isWindowOnlySetting, s as splitHostSettingsPatch, u as userScopeOverridesLoad } from "./host-scoped-settings-DxxI_yRo.js";
|
|
41
|
-
import { $ as createAutomaticInteractionResponse, $t as testDecisionModel, Bt as parsePluginScanPathEnv, D as resolveProviderConfig, G as readPinnedSkills, Ht as loadMcpToolsCache, It as resolveSessionCwd, J as ASK_USER_TOOL, Kt as subscribeMcpToolsCache, O as createFileEnvironmentSecrets, Ot as readRemoteSandboxRef, R as describeTitleStatus, Rt as buildPluginContributions, S as applyEnvironmentSettingsChange, St as builtinEnvironmentProviders, U as PINNED_SKILLS_META_KEY, Ut as mcpToolsCachePath, V as launchOverridesFrom, Wt as refreshMcpToolsCatalog, X as HOST_INTERACTION_ID_PREFIX, Xt as listDecisionModels, Z as PRESENT_PLAN_TOOL, Zt as parseDecisionModel, _t as discoverAgentsMd, a as tryOpenBrowser, c as mcpsWritePath, d as createExtensionMcpHost, dt as applyThinkingBindingSetting, f as resolveConfig, gt as installProviderDiagnosticHook, i as supportsOAuth, j as createSessionExecutionCache, l as removeMcpServer, lt as discoverProjectSkills, n as pickOAuthSelectOption, nt as isModelInteraction, o as tryRevealPath, p as resolveStoragePaths, r as runOAuthLogin, s as testModel, st as createSkillNameProbe, t as oauthUsesManualCodePaste, u as upsertMcpServer, ut as createBuiltinCommands, v as createFileMcpCredentialStore, w as describeEnvironments, x as provisionSessionEnvironment, zt as discoverProjectPlugins } from "./oauth-
|
|
41
|
+
import { $ as createAutomaticInteractionResponse, $t as testDecisionModel, Bt as parsePluginScanPathEnv, D as resolveProviderConfig, G as readPinnedSkills, Ht as loadMcpToolsCache, It as resolveSessionCwd, J as ASK_USER_TOOL, Kt as subscribeMcpToolsCache, O as createFileEnvironmentSecrets, Ot as readRemoteSandboxRef, R as describeTitleStatus, Rt as buildPluginContributions, S as applyEnvironmentSettingsChange, St as builtinEnvironmentProviders, U as PINNED_SKILLS_META_KEY, Ut as mcpToolsCachePath, V as launchOverridesFrom, Wt as refreshMcpToolsCatalog, X as HOST_INTERACTION_ID_PREFIX, Xt as listDecisionModels, Z as PRESENT_PLAN_TOOL, Zt as parseDecisionModel, _t as discoverAgentsMd, a as tryOpenBrowser, c as mcpsWritePath, d as createExtensionMcpHost, dt as applyThinkingBindingSetting, f as resolveConfig, gt as installProviderDiagnosticHook, i as supportsOAuth, j as createSessionExecutionCache, l as removeMcpServer, lt as discoverProjectSkills, n as pickOAuthSelectOption, nt as isModelInteraction, o as tryRevealPath, p as resolveStoragePaths, r as runOAuthLogin, s as testModel, st as createSkillNameProbe, t as oauthUsesManualCodePaste, u as upsertMcpServer, ut as createBuiltinCommands, v as createFileMcpCredentialStore, w as describeEnvironments, x as provisionSessionEnvironment, zt as discoverProjectPlugins } from "./oauth-BwPiX0hL.js";
|
|
42
42
|
import { F as commandNoteText, L as gatherCommandNotes, N as summarizeOutcomes, P as commandHeaderBlock, R as noteDisplay, S as previewEditPayload, _ as clipDiffForRender, b as extractEditPayload, g as buildUnifiedDiff, h as buildContextualDiff, l as promptSegmentLines, t as oneLineTitle, u as splitPromptSegments, w as summarizeEditPayload, x as filetypeFromPath } from "./session-title-text-DHCNlzjb.js";
|
|
43
43
|
import { D as usageByModel, S as sumRunCosts, a as eventsFromTurns, b as sessionEnvironmentSummary, c as headerFromData, d as loadState, f as marginTopFor, i as deriveSessionTitle, l as lastContextSizeFromTurns, o as getSessionHeader, p as noticeTurnContent, s as getSessionMeta, u as listSessionMeta, w as toolCallPreview } from "./store-BpkWIaSH.js";
|
|
44
44
|
import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-CbDaXIfU.js";
|
|
@@ -48,15 +48,15 @@ import { n as renderDefaultCallbackPage } from "./render-gPS08Uiz.js";
|
|
|
48
48
|
import { r as detectAuth } from "./cli-shared-GOehTeqk.js";
|
|
49
49
|
import { a as isMethodNotFound, n as LOCAL_HOST_KEY, r as LOCAL_HOST_NAME } from "./connection-BaHd3T-D.js";
|
|
50
50
|
import { d as loadAllEvents, f as loadEventsPage, m as loadSessionDocument, p as loadEventsTail, u as hostRefOf } from "./protocol-Do_i6Ras.js";
|
|
51
|
-
import { p as createHostEnvironment } from "./dispatcher-
|
|
51
|
+
import { p as createHostEnvironment } from "./dispatcher-D_9_QFO3.js";
|
|
52
52
|
import { a as SYNC_MODE_LABEL, c as settingField, d as syncMarkerOf, i as SYNC_MODES, n as SYNC_CATEGORIES, r as SYNC_CATEGORY_INFO, u as syncAgo } from "./types-B4oqzENY.js";
|
|
53
|
-
import { n as runRemoteOAuth, r as startInProcessHost, t as runWorkspaceExecStream } from "./workspace-exec-stream-
|
|
53
|
+
import { n as runRemoteOAuth, r as startInProcessHost, t as runWorkspaceExecStream } from "./workspace-exec-stream-Bzxczzu2.js";
|
|
54
54
|
import { t as keepHostConnected } from "./connection-loop-DSCehV3W.js";
|
|
55
55
|
import { $ as MACHINE_LINK_STATE_TEXT, D as cycleTab, Dt as MAX_FILE_ATTACHMENT_BYTES, F as togglePanel, Ft as HostRemovedError, H as groupPaletteRows, It as UnknownHostIdError, J as NOTICE_GLYPH, K as pushPaletteRecent, Lt as createHostIndex, Mt as parseSessionKey, Nt as sessionKey, Ot as MAX_IMAGE_ATTACHMENT_BYTES, P as resolveActiveTab, Q as validateMcpForm, St as indexOfProviderRow, T as buildPanelCatalog, Tt as selectableSources, Y as MCP_TRANSPORTS, _t as messagePreview, a as editTodo, at as machineWaiting, b as INITIAL_PANEL_STATE, c as EMPTY_RATE, ct as buildVisibleInstructionRows, d as streamRateValue, dt as foldSummary, f as createRunStopMarker, ft as foldsInCompactShape, ht as thoughtLabel, i as cycleTodoStatus, it as machineDirListing, j as pickTab, lt as instructionOriginBadge, m as ARTIFACTS_PANEL_ID, mt as planFold, n as buildTodoRetryMessage, nt as lastSeenText, ot as orderByMachine, p as tidyPromptText, pt as noteLabel, q as rankPaletteItems, r as clearTodos, rt as machineBlockedReason, s as removeTodo, t as addTodo, tt as environmentLabel, u as stepStreamRate, ut as instructionPreviewLines, wt as parentProviderKey, xt as buildVisibleAuthRows, yt as toolOverview } from "./todo-edits-DKoxu7Ss.js";
|
|
56
56
|
import { r as parseRemoteHostUrl, t as RemoteHostError } from "./url-D4vG7lzO.js";
|
|
57
57
|
import { createTuiStore } from "./session/sqlite.js";
|
|
58
|
-
import { $ as truncateTrailing, $t as groupArtifacts, At as buildRestartHint, B as useInteractionsActions, C as providerUsageSupported, Ct as claudeImportHostNote, D as fetchOAuthRedirect, Dt as previewClaudeImport, E as formatPathForCwd, Et as importFromClaudeCode, F as parentServerName, G as onboardingInstallSignals, H as connectorGithubToken, I as McpAuthProvider, J as onboardingProviders, L as useMcpAuthDispatch, Mt as useUpdateCheck, N as buildVisibleMcpRows, P as indexOfServerRow, Q as hintsLength, R as useMcpAuthState, Rt as useRestartRequired, Tt as hasClaudeCodeSetup, U as detectOnboardingEnvironment, V as useInteractionsQueue, X as clipHintsToWidth, Y as EMPTY_HINTS, Z as hintLayout, _ as withRunningCallStarts, _t as useConfig, a as useSurfaces, at as useEnabledToggleSet, b as fetchAllProviderUsage, c as useStreamBuffer, ct as useSettings, dt as createDiscoverySlot, et as fetchGithubUsage, f as writeSessionExport, ft as DiscoveryProvider, gt as ConfigProvider, h as useSafeModeQueue, ht as ConfigContext, i as useSelectStyle, it as listProjectFiles, j as useMcpToolToggleMap, jt as buildUpdateHint, k as redirectUriFromAuthUrl, kt as bootTick, l as aggregateUsage, lt as parseEditorCommand, m as useSafeModeActions, mt as useDiscoveryOptional, n as ThemeProvider, o as useSyntaxStyles, ot as SettingsOverride, p as SafeModeProvider, pt as useDiscovery, r as useColors, s as useTheme, st as SettingsProvider, t as computeTurnAnchors, tt as buildHints, ut as quoteForShell, w as usageChecksOptedOut, wt as countClaudeSessionFiles, x as fetchProviderUsage, xt as useCompletion, y as clearProviderUsageCache, yt as createArtifactsCompletionProvider, z as InteractionsProvider } from "./transcript-anchors-
|
|
59
|
-
import { i as wsTransport, n as lazyInProcessTransport, r as socketTransport, t as hostSignalHandlers } from "./host-signals-
|
|
58
|
+
import { $ as truncateTrailing, $t as groupArtifacts, At as buildRestartHint, B as useInteractionsActions, C as providerUsageSupported, Ct as claudeImportHostNote, D as fetchOAuthRedirect, Dt as previewClaudeImport, E as formatPathForCwd, Et as importFromClaudeCode, F as parentServerName, G as onboardingInstallSignals, H as connectorGithubToken, I as McpAuthProvider, J as onboardingProviders, L as useMcpAuthDispatch, Mt as useUpdateCheck, N as buildVisibleMcpRows, P as indexOfServerRow, Q as hintsLength, R as useMcpAuthState, Rt as useRestartRequired, Tt as hasClaudeCodeSetup, U as detectOnboardingEnvironment, V as useInteractionsQueue, X as clipHintsToWidth, Y as EMPTY_HINTS, Z as hintLayout, _ as withRunningCallStarts, _t as useConfig, a as useSurfaces, at as useEnabledToggleSet, b as fetchAllProviderUsage, c as useStreamBuffer, ct as useSettings, dt as createDiscoverySlot, et as fetchGithubUsage, f as writeSessionExport, ft as DiscoveryProvider, gt as ConfigProvider, h as useSafeModeQueue, ht as ConfigContext, i as useSelectStyle, it as listProjectFiles, j as useMcpToolToggleMap, jt as buildUpdateHint, k as redirectUriFromAuthUrl, kt as bootTick, l as aggregateUsage, lt as parseEditorCommand, m as useSafeModeActions, mt as useDiscoveryOptional, n as ThemeProvider, o as useSyntaxStyles, ot as SettingsOverride, p as SafeModeProvider, pt as useDiscovery, r as useColors, s as useTheme, st as SettingsProvider, t as computeTurnAnchors, tt as buildHints, ut as quoteForShell, w as usageChecksOptedOut, wt as countClaudeSessionFiles, x as fetchProviderUsage, xt as useCompletion, y as clearProviderUsageCache, yt as createArtifactsCompletionProvider, z as InteractionsProvider } from "./transcript-anchors-BcFLh3ZA.js";
|
|
59
|
+
import { i as wsTransport, n as lazyInProcessTransport, r as socketTransport, t as hostSignalHandlers } from "./host-signals-CUKPZZxC.js";
|
|
60
60
|
import { Buffer } from "node:buffer";
|
|
61
61
|
import * as fs from "node:fs";
|
|
62
62
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -11800,7 +11800,7 @@ function useRemoteHosts(deps) {
|
|
|
11800
11800
|
let cancelled = false;
|
|
11801
11801
|
let started = null;
|
|
11802
11802
|
remoteHostsLoads++;
|
|
11803
|
-
import("./remote-hosts-
|
|
11803
|
+
import("./remote-hosts-DcCh7fqp.js").then(({ startRemoteHosts }) => {
|
|
11804
11804
|
if (cancelled) return;
|
|
11805
11805
|
started = startRemoteHosts({
|
|
11806
11806
|
dataDir,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* retires it, so an upgrade can never leave windows silently executing on a
|
|
12
12
|
* process started from the previous release.
|
|
13
13
|
*/
|
|
14
|
-
const ZIDANE_VERSION = "8.2.
|
|
14
|
+
const ZIDANE_VERSION = "8.2.5";
|
|
15
15
|
/**
|
|
16
16
|
* Compare two dotted versions (`7.0.19`, `7.1.0-beta.1`): negative when `a`
|
|
17
17
|
* is older, positive when newer, 0 when equal. Pre-release tags are ignored
|
|
@@ -39,4 +39,4 @@ function parse(v) {
|
|
|
39
39
|
//#endregion
|
|
40
40
|
export { compareVersions as n, ZIDANE_VERSION as t };
|
|
41
41
|
|
|
42
|
-
//# sourceMappingURL=version-
|
|
42
|
+
//# sourceMappingURL=version-DOvIuU8M.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-
|
|
1
|
+
{"version":3,"file":"version-DOvIuU8M.js","names":["(pkg as { version?: string }).version"],"sources":["../package.json","../src/version.ts"],"sourcesContent":["","/**\n * The version of the zidane code this process is running — `package.json`\n * inlined at build time (Bun `--compile`, tsdown for `dist/`, and Vite for\n * the GUI main all fold JSON imports into the bundle, so this never reads\n * the filesystem at runtime and cannot report a NEIGHBOUR package's version\n * the way a `require('../package.json')` from a bundled chunk can).\n *\n * Used by the daemon handshake: a client running newer code than the daemon\n * retires it, so an upgrade can never leave windows silently executing on a\n * process started from the previous release.\n */\nimport pkg from '../package.json' with { type: 'json' }\n\nexport const ZIDANE_VERSION: string = (pkg as { version?: string }).version ?? '0.0.0'\n\n/**\n * Compare two dotted versions (`7.0.19`, `7.1.0-beta.1`): negative when `a`\n * is older, positive when newer, 0 when equal. Pre-release tags are ignored\n * — same release train, same code as far as the daemon fence is concerned.\n * Unparseable input compares equal (never fences on garbage).\n */\nexport function compareVersions(a: string, b: string): number {\n const pa = parse(a)\n const pb = parse(b)\n if (!pa || !pb)\n return 0\n for (let i = 0; i < 3; i++) {\n const d = (pa[i] ?? 0) - (pb[i] ?? 0)\n if (d !== 0)\n return d\n }\n return 0\n}\n\nfunction parse(v: string): number[] | null {\n const m = /^v?(\\d+)\\.(\\d+)\\.(\\d+)/.exec(v.trim())\n return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null\n}\n"],"mappings":";;;;;;;;;;;;;ACaA,MAAa,iBAAyBA;;;;;;;AAQtC,SAAgB,gBAAgB,GAAW,GAAmB;CAC5D,MAAM,KAAK,MAAM,CAAC;CAClB,MAAM,KAAK,MAAM,CAAC;CAClB,IAAI,CAAC,MAAM,CAAC,IACV,OAAO;CACT,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;EAC1B,MAAM,KAAK,GAAG,MAAM,MAAM,GAAG,MAAM;EACnC,IAAI,MAAM,GACR,OAAO;CACX;CACA,OAAO;AACT;AAEA,SAAS,MAAM,GAA4B;CACzC,MAAM,IAAI,yBAAyB,KAAK,EAAE,KAAK,CAAC;CAChD,OAAO,IAAI;EAAC,OAAO,EAAE,EAAE;EAAG,OAAO,EAAE,EAAE;EAAG,OAAO,EAAE,EAAE;CAAC,IAAI;AAC1D"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { S as startOAuthCallback } from "./mcp-BnXLqb2R.js";
|
|
2
|
-
import { t as ZIDANE_VERSION } from "./version-
|
|
2
|
+
import { t as ZIDANE_VERSION } from "./version-DOvIuU8M.js";
|
|
3
3
|
import { n as JsonRpcRemoteError, t as JsonRpcConnectionClosedError } from "./json-rpc-G0rBfEar.js";
|
|
4
4
|
import { a as isMethodNotFound } from "./connection-BaHd3T-D.js";
|
|
5
|
-
import { s as IPC_BUILD_ID } from "./settings-keys-
|
|
5
|
+
import { s as IPC_BUILD_ID } from "./settings-keys-CnrJgT1T.js";
|
|
6
6
|
import "./protocol-Do_i6Ras.js";
|
|
7
|
-
import { t as createHostDispatcher } from "./dispatcher-
|
|
7
|
+
import { t as createHostDispatcher } from "./dispatcher-D_9_QFO3.js";
|
|
8
8
|
//#region src/ipc/memory.ts
|
|
9
9
|
/** JSON round-trip: what crossing the socket does to a value. */
|
|
10
10
|
function wire(value) {
|
|
@@ -382,4 +382,4 @@ async function buffered(client, params, options, started) {
|
|
|
382
382
|
//#endregion
|
|
383
383
|
export { runRemoteOAuth as n, startInProcessHost as r, runWorkspaceExecStream as t };
|
|
384
384
|
|
|
385
|
-
//# sourceMappingURL=workspace-exec-stream-
|
|
385
|
+
//# sourceMappingURL=workspace-exec-stream-Bzxczzu2.js.map
|
package/dist/{workspace-exec-stream-iHv9cooF.js.map → workspace-exec-stream-Bzxczzu2.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-exec-stream-iHv9cooF.js","names":[],"sources":["../src/ipc/memory.ts","../src/ipc/remote-oauth.ts","../src/ipc/workspace-exec-stream.ts"],"sourcesContent":["/**\n * In-process transport: the host dispatcher in the window's own process, and\n * `IpcClient`s that call it directly instead of over a unix socket.\n *\n * For a window that runs its own host (TUI local mode on the shared engine):\n * the same dispatcher, method table and signal fan-out a daemon serves, with\n * no socket, no spawn and no second process. A caller sees what a socket\n * client would, so the daemon-mode code paths run on it unchanged:\n * - params, results and signals are JSON copies (as on the wire: a top-level\n * `undefined` result arrives as `null`, nested `undefined` fields are\n * dropped, and nothing is shared between the window and the host);\n * - a failed request rejects with `JsonRpcRemoteError` carrying the\n * dispatcher's code; a closed connection rejects with\n * `JsonRpcConnectionClosedError`, in-flight requests included;\n * - responses and signals arrive asynchronously, through one queue in the\n * order the host produced them (a socket reads them in write order);\n * - `close()` detaches quietly; the host shutting down calls `onClose`.\n *\n * Two socket behaviors are deliberately absent. Request deadlines exist on\n * the socket to recover from a wedged daemon by dropping the connection and\n * reconnecting; in-process there is nothing to reconnect to. And\n * `daemon.shutdown` from a client does not stop the host: its lifetime\n * belongs to the embedding window (`InProcessHost.close()`).\n */\nimport type { ChatHost } from '../chat/host/engine'\nimport type { IpcClient } from './client'\nimport type { HostPeer } from './daemon/dispatcher'\nimport type { DaemonEnvironment } from './daemon/environment'\nimport type { HelloResult, HostSignal } from './protocol'\nimport type { SignalListener } from './transports/client-core'\nimport { JsonRpcConnectionClosedError, JsonRpcRemoteError } from '../acp/json-rpc'\nimport { ZIDANE_VERSION } from '../version'\nimport { createHostDispatcher } from './daemon/dispatcher'\nimport { IPC_BUILD_ID } from './daemon/lifecycle'\nimport { IPC_PROTOCOL_VERSION } from './protocol'\n\n/** Options for {@link startInProcessHost}. */\nexport interface InProcessHostOptions {\n /** The host's environment, given the broadcast sink wired to client fan-out. */\n createEnvironment: (broadcast: (signal: HostSignal) => void) => DaemonEnvironment | Promise<DaemonEnvironment>\n /** Reported to clients as the host's `appVersion`. */\n appVersion?: string\n}\n\n/** Options for {@link InProcessHost.connect}. */\nexport interface InProcessClientOptions {\n /** Client version, surfaced in the host's diagnostics. */\n appVersion?: string\n /** Called when the host shuts down under a still-open client. */\n onClose?: () => void\n}\n\n/** A chat host running in the caller's process, with clients that call it directly. */\nexport interface InProcessHost {\n /** The chat host. */\n host: ChatHost\n /** A new client, already past the `ipc.hello` handshake. */\n connect: (options?: InProcessClientOptions) => Promise<IpcClient>\n /** Number of open clients. */\n peerCount: () => number\n /**\n * Shut the host down: refuse new work, abort and await every run, then\n * close the clients still open (their `onClose` fires). Idempotent.\n */\n close: () => Promise<void>\n}\n\n/** JSON round-trip: what crossing the socket does to a value. */\nfunction wire<T>(value: T): T {\n return value === undefined ? (null as T) : JSON.parse(JSON.stringify(value)) as T\n}\n\n/** One open client, from the host's side. */\ninterface Connection {\n peer: HostPeer\n /** The host is shutting down: reject what is pending, then tell the client. */\n hostClosed: () => void\n}\n\n/**\n * Starts a chat host in this process. Clients from `connect` behave like socket clients, without a socket or a second process.\n * @param options - Environment factory and version.\n * @returns The host handle.\n */\nexport async function startInProcessHost(options: InProcessHostOptions): Promise<InProcessHost> {\n const connections = new Set<Connection>()\n let closing: Promise<void> | null = null\n\n const dispatcher = await createHostDispatcher({\n createEnvironment: options.createEnvironment,\n appVersion: options.appVersion ?? 'dev',\n startedAt: Date.now(),\n identity: { kind: 'in-process' },\n lifecycle: {\n // Owned by the embedding window, see the module header.\n shutdown: () => undefined,\n // Clients are this build by construction: never a newer one to retire for.\n retireWhenIdle: () => undefined,\n isRetiring: () => false,\n },\n })\n\n async function connect(clientOptions: InProcessClientOptions = {}): Promise<IpcClient> {\n if (closing)\n throw new JsonRpcConnectionClosedError()\n const listeners = new Set<SignalListener>()\n const pending = new Set<(err: Error) => void>()\n let closed = false\n\n /*\n * Everything bound for the client goes through one FIFO drained in a\n * microtask: signals, and request outcomes once the dispatcher settles\n * them. A signal the host emits after answering a request is therefore\n * delivered after that answer, as a socket would read them.\n */\n const outbox: Array<() => void> = []\n let draining = false\n const deliver = (item: () => void): void => {\n outbox.push(item)\n if (draining)\n return\n draining = true\n queueMicrotask(() => {\n try {\n while (outbox.length > 0) {\n try {\n outbox.shift()!()\n }\n catch {\n // A throwing listener costs that one message, as on the socket\n // (the JSON-RPC reader catches it), never the connection.\n }\n }\n }\n finally {\n draining = false\n }\n })\n }\n\n const peer: HostPeer = {\n ready: false,\n watching: null,\n send(signal, seq, encoded) {\n if (closed)\n return\n const meta = seq === undefined ? undefined : { seq }\n let copy: HostSignal\n try {\n // The fan-out's one serialization, when it has one.\n const json = encoded ? encoded.json() : JSON.stringify(signal)\n if (json === undefined)\n return\n copy = JSON.parse(json) as HostSignal\n }\n catch {\n // Not JSON-able: the socket server drops it the same way, and a\n // bad signal must never break fan-out to the other clients.\n return\n }\n deliver(() => {\n if (closed)\n return\n for (const listener of listeners)\n listener(copy, meta)\n })\n },\n }\n\n const request = (method: string, params: unknown): Promise<unknown> => {\n if (closed)\n return Promise.reject(new JsonRpcConnectionClosedError())\n return new Promise((resolve, reject) => {\n pending.add(reject)\n const settle = (outcome: () => void): void => {\n deliver(() => {\n if (!pending.delete(reject))\n return\n outcome()\n })\n }\n dispatcher.handleRequest(method, params === undefined ? undefined : wire(params), peer).then(\n (result) => {\n settle(() => {\n // The reject is already out of `pending`: a throw here (a result\n // that is not JSON-able) must fail the request, not hang it.\n try {\n if (result.ok)\n resolve(wire(result.result))\n else\n reject(new JsonRpcRemoteError(result.message, result.code, result.data))\n }\n catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n }\n })\n },\n err => settle(() => reject(err)),\n )\n })\n }\n\n /** Stop delivering and fail what is in flight, as a socket closing does. */\n const shut = (): void => {\n closed = true\n listeners.clear()\n outbox.length = 0\n for (const reject of pending)\n reject(new JsonRpcConnectionClosedError())\n pending.clear()\n }\n\n const connection: Connection = {\n peer,\n hostClosed: () => {\n if (closed)\n return\n shut()\n clientOptions.onClose?.()\n },\n }\n\n dispatcher.addPeer(peer)\n connections.add(connection)\n const detach = (): void => {\n if (closed)\n return\n shut()\n connections.delete(connection)\n dispatcher.removePeer(peer)\n }\n\n let hello: HelloResult\n try {\n hello = await request('ipc.hello', {\n protocolVersion: IPC_PROTOCOL_VERSION,\n appVersion: clientOptions.appVersion ?? 'dev',\n pid: process.pid,\n buildId: IPC_BUILD_ID,\n codeVersion: ZIDANE_VERSION,\n }) as HelloResult\n }\n catch (err) {\n detach()\n throw err\n }\n\n return {\n hello,\n request: request as IpcClient['request'],\n notify: (method, params) => {\n if (closed)\n return\n const copy = params === undefined ? undefined : wire(params)\n // Off the caller's stack, as a socket write would be.\n queueMicrotask(() => {\n if (!closed)\n void dispatcher.handleNotification(method, copy, peer)\n })\n },\n onSignal: (listener) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n close: detach,\n }\n }\n\n function close(): Promise<void> {\n // Same order as the socket server's shutdown: drain the host, close\n // every connection, then forget the peers without per-peer cleanup.\n closing ??= (async () => {\n await dispatcher.host.drain()\n for (const connection of connections)\n connection.hostClosed()\n connections.clear()\n dispatcher.detachAll()\n })()\n return closing\n }\n\n return {\n host: dispatcher.host,\n connect,\n peerCount: () => dispatcher.peerCount(),\n close,\n }\n}\n","/**\n * The client half of a remote OAuth sign-in (`auth.oauth.*`): the host runs\n * the login and keeps its secrets; this machine has the browser. For an MCP\n * server it opens a loopback listener first (any free port) and hands its\n * URI to the host; for a provider it listens on the provider's fixed\n * `redirect_uri` once the host names it (a busy port falls back to pasting\n * the address the browser lands on). It opens the URL, forwards the code\n * (or what the user pasted) to the host, and resolves with the host's\n * outcome, which never carries a token. Device and polling flows just wait.\n *\n * The provider's own questions (a choice between flows, a free-text\n * prompt: `HostOAuthStep`) come back from `begin` / `complete`; the UI\n * answers them (`select` / `prompt`; a prompt falls back to `paste`). A UI\n * that can't answers nothing: the sign-in ends with why, never a default.\n */\nimport type { McpOAuthCallbackPageRenderer, OAuthCallbackHandle } from '../mcp/oauth-callback'\nimport type { HostOAuthBegin, HostOAuthComplete, HostOAuthStep } from './protocol'\nimport type { IpcClient } from './transports/client-core'\nimport { startOAuthCallback } from '../mcp/oauth-callback'\n\n/** What to sign in to: a provider by key, or an MCP server by name. */\nexport type RemoteOAuthTarget = { provider: string } | { mcpServer: string, cwd?: string, kind?: string }\n\nexport interface RemoteOAuthStart extends HostOAuthBegin {\n /** This machine catches the redirect (else: paste, or wait for a device / polling flow). */\n listening: boolean\n /** Why it doesn't (a busy port). */\n listenError?: string\n}\n\n/** Hooks a window supplies to {@link runRemoteOAuth}: open the browser, take pasted input, answer the provider's questions, and abort. */\nexport interface RemoteOAuthUi {\n /** Open the browser here (best effort; the URL is shown too). */\n openUrl: (url: string) => void | Promise<void>\n /** The flow started: the URL, the mode, a device code, whether the loopback listens. */\n onStart?: (start: RemoteOAuthStart) => void\n /** What the user pastes (a code or the address the browser landed on); offered in every mode. */\n paste?: () => Promise<string>\n /** Aborts the sign-in. The run rejects with \"OAuth login aborted\". */\n signal?: AbortSignal\n /** The page the browser lands on at the loopback. */\n renderPage?: McpOAuthCallbackPageRenderer\n /** The provider asks the user to choose (its option id; `undefined` cancels). */\n select?: (step: Extract<HostOAuthStep, { kind: 'select' }>) => Promise<string | undefined>\n /** The provider asks the user a question (default: {@link paste}). */\n prompt?: (step: Extract<HostOAuthStep, { kind: 'prompt' }>) => Promise<string>\n}\n\n/** The UI's answer to `step`, or why it can't give one. */\nasync function answerStep(ui: RemoteOAuthUi, step: HostOAuthStep): Promise<string | undefined> {\n if (step.kind === 'select' && ui.select)\n return ui.select(step)\n if (step.kind === 'prompt' && (ui.prompt || ui.paste))\n return ui.prompt ? ui.prompt(step) : ui.paste!()\n throw new Error(`The sign-in asks \"${step.message}\", and this window can't answer it here: sign in on the server machine or use an API key.`)\n}\n\nconst abortError = (): Error => new Error('OAuth login aborted')\n\nfunction whenAborted(signal: AbortSignal | undefined): Promise<never> {\n return new Promise<never>((_, reject) => {\n if (signal?.aborted)\n reject(abortError())\n signal?.addEventListener('abort', () => reject(abortError()), { once: true })\n })\n}\n\n/**\n * Runs the client half of a remote OAuth sign-in against a host.\n * Starts the flow on the host, opens the URL through the UI, forwards the code from the loopback listener or from `paste`, and resolves with the host's outcome, which never carries a token. Rejects when aborted or when the UI cannot answer a question the provider asks.\n * @param client - Client used for the `auth.oauth.*` requests.\n * @param target - Provider or MCP server to sign in to.\n * @param ui - Browser, prompt and abort hooks.\n * @returns The sign-in outcome: the provider status or the MCP server name.\n */\nexport async function runRemoteOAuth(\n client: Pick<IpcClient, 'request'>,\n target: RemoteOAuthTarget,\n ui: RemoteOAuthUi,\n): Promise<HostOAuthComplete> {\n const { signal } = ui\n signal?.throwIfAborted()\n const isMcp = 'mcpServer' in target\n let loopback: OAuthCallbackHandle | undefined\n const listenOptions = { ...(signal ? { signal } : {}), ...(ui.renderPage ? { renderPage: ui.renderPage } : {}), provider: isMcp ? target.mcpServer : target.provider }\n if (isMcp)\n loopback = await startOAuthCallback({ ...listenOptions, port: 0 })\n let begin: HostOAuthBegin | undefined\n try {\n begin = await client.request('auth.oauth.begin', isMcp\n ? { mcpServer: target.mcpServer, ...(target.cwd !== undefined ? { cwd: target.cwd } : {}), ...(target.kind !== undefined ? { kind: target.kind } : {}), redirectUri: loopback!.redirectUri }\n : { provider: target.provider })\n const flowId = begin.flowId\n const respond = async (step: HostOAuthStep): Promise<HostOAuthBegin> => {\n const value = await answerStep(ui, step)\n signal?.throwIfAborted()\n return client.request('auth.oauth.respond', { flowId, stepId: step.id, ...(value !== undefined ? { value } : {}) })\n }\n // Questions before the URL.\n while (begin.step)\n begin = await respond(begin.step)\n let listenError: string | undefined\n // The host pinned another redirect (an MCP server's pre-registered\n // client, `oauth.callbackPort`): listen there instead.\n if (loopback && begin.redirectUri && begin.redirectUri !== loopback.redirectUri) {\n loopback.promise.catch(() => undefined) // closing rejects it; nothing waits on it\n await loopback.close()\n loopback = undefined\n }\n if (!loopback && begin.mode === 'loopback' && begin.redirectUri) {\n const redirect = new URL(begin.redirectUri)\n try {\n loopback = await startOAuthCallback({ ...listenOptions, port: Number(redirect.port), path: redirect.pathname })\n }\n catch (err) {\n listenError = `Port ${redirect.port} is busy on this computer (${err instanceof Error ? err.message : String(err)}): paste the address the browser lands on instead.`\n }\n }\n ui.onStart?.({ ...begin, listening: loopback !== undefined, ...(listenError ? { listenError } : {}) })\n await ui.openUrl(begin.url)\n\n /** Deliver (once), then wait; a question after the URL is answered and the wait goes on. */\n const complete = async (params: { code?: string, state?: string, input?: string }): Promise<HostOAuthComplete> => {\n let outcome = await client.request('auth.oauth.complete', { flowId, ...params })\n while (outcome.step) {\n await respond(outcome.step)\n outcome = await client.request('auth.oauth.complete', { flowId })\n }\n return outcome\n }\n const ways: Promise<HostOAuthComplete>[] = [whenAborted(signal)]\n if (loopback) {\n const handle = loopback\n ways.push(handle.promise.then(({ code, state }) => complete({ code, ...(state !== undefined ? { state } : {}) })))\n }\n if (ui.paste)\n ways.push(ui.paste().then(input => complete({ input })))\n // Device and polling flows finish on the host's side: wait for it.\n if (begin.mode !== 'loopback')\n ways.push(complete({}))\n for (const way of ways)\n way.catch(() => undefined)\n return await Promise.race(ways)\n }\n catch (err) {\n if (begin)\n void client.request('auth.oauth.cancel', { flowId: begin.flowId }).catch(() => undefined)\n throw err\n }\n finally {\n await loopback?.close()\n }\n}\n","/**\n * The client half of a streamed workspace command (`!` on a remote host):\n * starts `sessions.workspaceExecStream`, gathers its `workspace:exec:*`\n * signals (which may beat the reply) into one output in arrival order, and\n * cancels on the host when `signal` aborts. The result has the shape of a\n * local `!` run (`BashRunResult`), so both format and persist alike. A host\n * without the method gets the buffered `sessions.workspaceExec`, where an\n * abort stops waiting, not the command.\n */\nimport type { HostSignal } from './protocol'\nimport type { IpcClient } from './transports/client-core'\nimport { isMethodNotFound } from '../chat/hosts/connection'\n\n/** Outcome of {@link runWorkspaceExecStream}, shaped like a local `!` run. */\nexport interface WorkspaceExecStreamResult {\n /** stdout and stderr in arrival order (the buffered fallback: stdout, then stderr). */\n output: string\n /** `null` when the host killed it (cancel, output cap). */\n exitCode: number | null\n /** Wall-clock time of the run in milliseconds. */\n durationMs: number\n /** Cancelled through `signal`. */\n aborted: boolean\n /** The host's output cap was reached and the process stopped. */\n overflowed: boolean\n /** `false`: an older host answered with the buffered exec. */\n streamed: boolean\n}\n\n/** Options for {@link runWorkspaceExecStream}. */\nexport interface WorkspaceExecStreamOptions {\n /** Cancels the command on the host when aborted. */\n signal?: AbortSignal\n /** The whole output so far, on every chunk. */\n onChunk?: (accumulated: string) => void\n /** The buffered fallback's bound. Default 600 s. */\n fallbackTimeoutSeconds?: number\n /** How long to wait for the host's exit after a cancel before giving up on it. Default 10 s. */\n cancelGraceMs?: number\n}\n\ntype ExecSignal = Extract<HostSignal, { kind: 'workspace:exec:data' | 'workspace:exec:exit' }>\n\n/**\n * Runs a command in a session's workspace on a host and streams its output.\n * The result has the shape of a local `!` run. A host without `sessions.workspaceExecStream` gets the buffered `sessions.workspaceExec` instead, and then an abort stops the wait, not the command.\n * @param client - Client used for requests and signals.\n * @param params - Session, command and optional working directory.\n * @param options - Abort signal, chunk callback and timeouts.\n * @returns The output, exit code and flags describing how it ended.\n */\nexport async function runWorkspaceExecStream(\n client: Pick<IpcClient, 'request' | 'onSignal'>,\n params: { sessionId: string, command: string, cwd?: string },\n options: WorkspaceExecStreamOptions = {},\n): Promise<WorkspaceExecStreamResult> {\n const { signal } = options\n const started = Date.now()\n signal?.throwIfAborted()\n\n let execId: string | null = null\n /** Signals that arrived before the reply named the exec. */\n const early: ExecSignal[] = []\n let output = ''\n let finish: (exit: Extract<ExecSignal, { kind: 'workspace:exec:exit' }> | null) => void = () => undefined\n const exited = new Promise<Extract<ExecSignal, { kind: 'workspace:exec:exit' }> | null>((resolve) => {\n finish = resolve\n })\n const apply = (event: ExecSignal): void => {\n if (event.kind === 'workspace:exec:data') {\n output += event.chunk\n options.onChunk?.(output)\n return\n }\n finish(event)\n }\n const unsubscribe = client.onSignal((event) => {\n if (event.kind !== 'workspace:exec:data' && event.kind !== 'workspace:exec:exit')\n return\n if (execId === null)\n early.push(event)\n else if (event.execId === execId)\n apply(event)\n })\n\n try {\n try {\n execId = (await client.request('sessions.workspaceExecStream', { sessionId: params.sessionId, command: params.command, ...(params.cwd !== undefined ? { cwd: params.cwd } : {}) })).execId\n }\n catch (err) {\n if (!isMethodNotFound(err))\n throw err\n return await buffered(client, params, options, started)\n }\n for (const event of early.splice(0)) {\n if (event.execId === execId)\n apply(event)\n }\n\n const id = execId\n const onAbort = (): void => {\n void client.request('sessions.workspaceExecCancel', { execId: id }).catch(() => undefined)\n // The exit normally follows the kill; a connection lost meanwhile must not hang the caller.\n setTimeout(finish, options.cancelGraceMs ?? 10_000, null).unref?.()\n }\n signal?.addEventListener('abort', onAbort, { once: true })\n if (signal?.aborted)\n onAbort()\n let exit: Awaited<typeof exited>\n try {\n exit = await exited\n }\n finally {\n signal?.removeEventListener('abort', onAbort)\n }\n return {\n output,\n exitCode: exit === null || exit.signal !== null ? null : exit.code,\n durationMs: Date.now() - started,\n aborted: signal?.aborted === true,\n overflowed: exit?.truncated === true,\n streamed: true,\n }\n }\n finally {\n unsubscribe()\n }\n}\n\n/** An older host: one buffered exec; `signal` stops waiting for it. */\nasync function buffered(\n client: Pick<IpcClient, 'request'>,\n params: { sessionId: string, command: string, cwd?: string },\n options: WorkspaceExecStreamOptions,\n started: number,\n): Promise<WorkspaceExecStreamResult> {\n const { signal } = options\n const request = client.request('sessions.workspaceExec', {\n sessionId: params.sessionId,\n command: params.command,\n ...(params.cwd !== undefined ? { cwd: params.cwd } : {}),\n timeoutSeconds: options.fallbackTimeoutSeconds ?? 600,\n })\n const stopped = new Promise<null>((resolve) => {\n if (signal?.aborted)\n resolve(null)\n signal?.addEventListener('abort', () => resolve(null), { once: true })\n })\n request.catch(() => undefined)\n const result = await Promise.race([request, stopped])\n if (!result)\n return { output: '', exitCode: null, durationMs: Date.now() - started, aborted: true, overflowed: false, streamed: false }\n const output = [result.stdout, result.stderr].filter(part => part.trim() !== '').map(part => part.replace(/\\n$/, '')).join('\\n')\n options.onChunk?.(output)\n return { output, exitCode: result.exitCode, durationMs: Date.now() - started, aborted: false, overflowed: false, streamed: false }\n}\n"],"mappings":";;;;;;;;;AAoEA,SAAS,KAAQ,OAAa;CAC5B,OAAO,UAAU,KAAA,IAAa,OAAa,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAC7E;;;;;;AAcA,eAAsB,mBAAmB,SAAuD;CAC9F,MAAM,8BAAc,IAAI,IAAgB;CACxC,IAAI,UAAgC;CAEpC,MAAM,aAAa,MAAM,qBAAqB;EAC5C,mBAAmB,QAAQ;EAC3B,YAAY,QAAQ,cAAc;EAClC,WAAW,KAAK,IAAI;EACpB,UAAU,EAAE,MAAM,aAAa;EAC/B,WAAW;GAET,gBAAgB,KAAA;GAEhB,sBAAsB,KAAA;GACtB,kBAAkB;EACpB;CACF,CAAC;CAED,eAAe,QAAQ,gBAAwC,CAAC,GAAuB;EACrF,IAAI,SACF,MAAM,IAAI,6BAA6B;EACzC,MAAM,4BAAY,IAAI,IAAoB;EAC1C,MAAM,0BAAU,IAAI,IAA0B;EAC9C,IAAI,SAAS;EAQb,MAAM,SAA4B,CAAC;EACnC,IAAI,WAAW;EACf,MAAM,WAAW,SAA2B;GAC1C,OAAO,KAAK,IAAI;GAChB,IAAI,UACF;GACF,WAAW;GACX,qBAAqB;IACnB,IAAI;KACF,OAAO,OAAO,SAAS,GACrB,IAAI;MACF,OAAO,MAAM,CAAC,CAAE;KAClB,QACM,CAGN;IAEJ,UACQ;KACN,WAAW;IACb;GACF,CAAC;EACH;EAEA,MAAM,OAAiB;GACrB,OAAO;GACP,UAAU;GACV,KAAK,QAAQ,KAAK,SAAS;IACzB,IAAI,QACF;IACF,MAAM,OAAO,QAAQ,KAAA,IAAY,KAAA,IAAY,EAAE,IAAI;IACnD,IAAI;IACJ,IAAI;KAEF,MAAM,OAAO,UAAU,QAAQ,KAAK,IAAI,KAAK,UAAU,MAAM;KAC7D,IAAI,SAAS,KAAA,GACX;KACF,OAAO,KAAK,MAAM,IAAI;IACxB,QACM;KAGJ;IACF;IACA,cAAc;KACZ,IAAI,QACF;KACF,KAAK,MAAM,YAAY,WACrB,SAAS,MAAM,IAAI;IACvB,CAAC;GACH;EACF;EAEA,MAAM,WAAW,QAAgB,WAAsC;GACrE,IAAI,QACF,OAAO,QAAQ,OAAO,IAAI,6BAA6B,CAAC;GAC1D,OAAO,IAAI,SAAS,SAAS,WAAW;IACtC,QAAQ,IAAI,MAAM;IAClB,MAAM,UAAU,YAA8B;KAC5C,cAAc;MACZ,IAAI,CAAC,QAAQ,OAAO,MAAM,GACxB;MACF,QAAQ;KACV,CAAC;IACH;IACA,WAAW,cAAc,QAAQ,WAAW,KAAA,IAAY,KAAA,IAAY,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,MACrF,WAAW;KACV,aAAa;MAGX,IAAI;OACF,IAAI,OAAO,IACT,QAAQ,KAAK,OAAO,MAAM,CAAC;YAE3B,OAAO,IAAI,mBAAmB,OAAO,SAAS,OAAO,MAAM,OAAO,IAAI,CAAC;MAC3E,SACO,KAAK;OACV,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;MAC5D;KACF,CAAC;IACH,IACA,QAAO,aAAa,OAAO,GAAG,CAAC,CACjC;GACF,CAAC;EACH;;EAGA,MAAM,aAAmB;GACvB,SAAS;GACT,UAAU,MAAM;GAChB,OAAO,SAAS;GAChB,KAAK,MAAM,UAAU,SACnB,OAAO,IAAI,6BAA6B,CAAC;GAC3C,QAAQ,MAAM;EAChB;EAEA,MAAM,aAAyB;GAC7B;GACA,kBAAkB;IAChB,IAAI,QACF;IACF,KAAK;IACL,cAAc,UAAU;GAC1B;EACF;EAEA,WAAW,QAAQ,IAAI;EACvB,YAAY,IAAI,UAAU;EAC1B,MAAM,eAAqB;GACzB,IAAI,QACF;GACF,KAAK;GACL,YAAY,OAAO,UAAU;GAC7B,WAAW,WAAW,IAAI;EAC5B;EAEA,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,QAAQ,aAAa;IACjC,iBAAA;IACA,YAAY,cAAc,cAAc;IACxC,KAAK,QAAQ;IACb,SAAS;IACT,aAAa;GACf,CAAC;EACH,SACO,KAAK;GACV,OAAO;GACP,MAAM;EACR;EAEA,OAAO;GACL;GACS;GACT,SAAS,QAAQ,WAAW;IAC1B,IAAI,QACF;IACF,MAAM,OAAO,WAAW,KAAA,IAAY,KAAA,IAAY,KAAK,MAAM;IAE3D,qBAAqB;KACnB,IAAI,CAAC,QACH,WAAgB,mBAAmB,QAAQ,MAAM,IAAI;IACzD,CAAC;GACH;GACA,WAAW,aAAa;IACtB,UAAU,IAAI,QAAQ;IACtB,aAAa,UAAU,OAAO,QAAQ;GACxC;GACA,OAAO;EACT;CACF;CAEA,SAAS,QAAuB;EAG9B,aAAa,YAAY;GACvB,MAAM,WAAW,KAAK,MAAM;GAC5B,KAAK,MAAM,cAAc,aACvB,WAAW,WAAW;GACxB,YAAY,MAAM;GAClB,WAAW,UAAU;EACvB,EAAA,CAAG;EACH,OAAO;CACT;CAEA,OAAO;EACL,MAAM,WAAW;EACjB;EACA,iBAAiB,WAAW,UAAU;EACtC;CACF;AACF;;;;AC9OA,eAAe,WAAW,IAAmB,MAAkD;CAC7F,IAAI,KAAK,SAAS,YAAY,GAAG,QAC/B,OAAO,GAAG,OAAO,IAAI;CACvB,IAAI,KAAK,SAAS,aAAa,GAAG,UAAU,GAAG,QAC7C,OAAO,GAAG,SAAS,GAAG,OAAO,IAAI,IAAI,GAAG,MAAO;CACjD,MAAM,IAAI,MAAM,qBAAqB,KAAK,QAAQ,0FAA0F;AAC9I;AAEA,MAAM,mCAA0B,IAAI,MAAM,qBAAqB;AAE/D,SAAS,YAAY,QAAiD;CACpE,OAAO,IAAI,SAAgB,GAAG,WAAW;EACvC,IAAI,QAAQ,SACV,OAAO,WAAW,CAAC;EACrB,QAAQ,iBAAiB,eAAe,OAAO,WAAW,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC;CAC9E,CAAC;AACH;;;;;;;;;AAUA,eAAsB,eACpB,QACA,QACA,IAC4B;CAC5B,MAAM,EAAE,WAAW;CACnB,QAAQ,eAAe;CACvB,MAAM,QAAQ,eAAe;CAC7B,IAAI;CACJ,MAAM,gBAAgB;EAAE,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAAI,GAAI,GAAG,aAAa,EAAE,YAAY,GAAG,WAAW,IAAI,CAAC;EAAI,UAAU,QAAQ,OAAO,YAAY,OAAO;CAAS;CACrK,IAAI,OACF,WAAW,MAAM,mBAAmB;EAAE,GAAG;EAAe,MAAM;CAAE,CAAC;CACnE,IAAI;CACJ,IAAI;EACF,QAAQ,MAAM,OAAO,QAAQ,oBAAoB,QAC7C;GAAE,WAAW,OAAO;GAAW,GAAI,OAAO,QAAQ,KAAA,IAAY,EAAE,KAAK,OAAO,IAAI,IAAI,CAAC;GAAI,GAAI,OAAO,SAAS,KAAA,IAAY,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;GAAI,aAAa,SAAU;EAAY,IACzL,EAAE,UAAU,OAAO,SAAS,CAAC;EACjC,MAAM,SAAS,MAAM;EACrB,MAAM,UAAU,OAAO,SAAiD;GACtE,MAAM,QAAQ,MAAM,WAAW,IAAI,IAAI;GACvC,QAAQ,eAAe;GACvB,OAAO,OAAO,QAAQ,sBAAsB;IAAE;IAAQ,QAAQ,KAAK;IAAI,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC;EACpH;EAEA,OAAO,MAAM,MACX,QAAQ,MAAM,QAAQ,MAAM,IAAI;EAClC,IAAI;EAGJ,IAAI,YAAY,MAAM,eAAe,MAAM,gBAAgB,SAAS,aAAa;GAC/E,SAAS,QAAQ,YAAY,KAAA,CAAS;GACtC,MAAM,SAAS,MAAM;GACrB,WAAW,KAAA;EACb;EACA,IAAI,CAAC,YAAY,MAAM,SAAS,cAAc,MAAM,aAAa;GAC/D,MAAM,WAAW,IAAI,IAAI,MAAM,WAAW;GAC1C,IAAI;IACF,WAAW,MAAM,mBAAmB;KAAE,GAAG;KAAe,MAAM,OAAO,SAAS,IAAI;KAAG,MAAM,SAAS;IAAS,CAAC;GAChH,SACO,KAAK;IACV,cAAc,QAAQ,SAAS,KAAK,6BAA6B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;GACpH;EACF;EACA,GAAG,UAAU;GAAE,GAAG;GAAO,WAAW,aAAa,KAAA;GAAW,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;EAAG,CAAC;EACrG,MAAM,GAAG,QAAQ,MAAM,GAAG;;EAG1B,MAAM,WAAW,OAAO,WAA0F;GAChH,IAAI,UAAU,MAAM,OAAO,QAAQ,uBAAuB;IAAE;IAAQ,GAAG;GAAO,CAAC;GAC/E,OAAO,QAAQ,MAAM;IACnB,MAAM,QAAQ,QAAQ,IAAI;IAC1B,UAAU,MAAM,OAAO,QAAQ,uBAAuB,EAAE,OAAO,CAAC;GAClE;GACA,OAAO;EACT;EACA,MAAM,OAAqC,CAAC,YAAY,MAAM,CAAC;EAC/D,IAAI,UAAU;GACZ,MAAM,SAAS;GACf,KAAK,KAAK,OAAO,QAAQ,MAAM,EAAE,MAAM,YAAY,SAAS;IAAE;IAAM,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC,CAAC,CAAC;EACnH;EACA,IAAI,GAAG,OACL,KAAK,KAAK,GAAG,MAAM,CAAC,CAAC,MAAK,UAAS,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;EAEzD,IAAI,MAAM,SAAS,YACjB,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC;EACxB,KAAK,MAAM,OAAO,MAChB,IAAI,YAAY,KAAA,CAAS;EAC3B,OAAO,MAAM,QAAQ,KAAK,IAAI;CAChC,SACO,KAAK;EACV,IAAI,OACF,OAAY,QAAQ,qBAAqB,EAAE,QAAQ,MAAM,OAAO,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;EAC1F,MAAM;CACR,UACQ;EACN,MAAM,UAAU,MAAM;CACxB;AACF;;;;;;;;;;;ACrGA,eAAsB,uBACpB,QACA,QACA,UAAsC,CAAC,GACH;CACpC,MAAM,EAAE,WAAW;CACnB,MAAM,UAAU,KAAK,IAAI;CACzB,QAAQ,eAAe;CAEvB,IAAI,SAAwB;;CAE5B,MAAM,QAAsB,CAAC;CAC7B,IAAI,SAAS;CACb,IAAI,eAA4F,KAAA;CAChG,MAAM,SAAS,IAAI,SAAsE,YAAY;EACnG,SAAS;CACX,CAAC;CACD,MAAM,SAAS,UAA4B;EACzC,IAAI,MAAM,SAAS,uBAAuB;GACxC,UAAU,MAAM;GAChB,QAAQ,UAAU,MAAM;GACxB;EACF;EACA,OAAO,KAAK;CACd;CACA,MAAM,cAAc,OAAO,UAAU,UAAU;EAC7C,IAAI,MAAM,SAAS,yBAAyB,MAAM,SAAS,uBACzD;EACF,IAAI,WAAW,MACb,MAAM,KAAK,KAAK;OACb,IAAI,MAAM,WAAW,QACxB,MAAM,KAAK;CACf,CAAC;CAED,IAAI;EACF,IAAI;GACF,UAAU,MAAM,OAAO,QAAQ,gCAAgC;IAAE,WAAW,OAAO;IAAW,SAAS,OAAO;IAAS,GAAI,OAAO,QAAQ,KAAA,IAAY,EAAE,KAAK,OAAO,IAAI,IAAI,CAAC;GAAG,CAAC,EAAA,CAAG;EACtL,SACO,KAAK;GACV,IAAI,CAAC,iBAAiB,GAAG,GACvB,MAAM;GACR,OAAO,MAAM,SAAS,QAAQ,QAAQ,SAAS,OAAO;EACxD;EACA,KAAK,MAAM,SAAS,MAAM,OAAO,CAAC,GAChC,IAAI,MAAM,WAAW,QACnB,MAAM,KAAK;EAGf,MAAM,KAAK;EACX,MAAM,gBAAsB;GAC1B,OAAY,QAAQ,gCAAgC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;GAEzF,WAAW,QAAQ,QAAQ,iBAAiB,KAAQ,IAAI,CAAC,CAAC,QAAQ;EACpE;EACA,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EACzD,IAAI,QAAQ,SACV,QAAQ;EACV,IAAI;EACJ,IAAI;GACF,OAAO,MAAM;EACf,UACQ;GACN,QAAQ,oBAAoB,SAAS,OAAO;EAC9C;EACA,OAAO;GACL;GACA,UAAU,SAAS,QAAQ,KAAK,WAAW,OAAO,OAAO,KAAK;GAC9D,YAAY,KAAK,IAAI,IAAI;GACzB,SAAS,QAAQ,YAAY;GAC7B,YAAY,MAAM,cAAc;GAChC,UAAU;EACZ;CACF,UACQ;EACN,YAAY;CACd;AACF;;AAGA,eAAe,SACb,QACA,QACA,SACA,SACoC;CACpC,MAAM,EAAE,WAAW;CACnB,MAAM,UAAU,OAAO,QAAQ,0BAA0B;EACvD,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,GAAI,OAAO,QAAQ,KAAA,IAAY,EAAE,KAAK,OAAO,IAAI,IAAI,CAAC;EACtD,gBAAgB,QAAQ,0BAA0B;CACpD,CAAC;CACD,MAAM,UAAU,IAAI,SAAe,YAAY;EAC7C,IAAI,QAAQ,SACV,QAAQ,IAAI;EACd,QAAQ,iBAAiB,eAAe,QAAQ,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;CACvE,CAAC;CACD,QAAQ,YAAY,KAAA,CAAS;CAC7B,MAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,SAAS,OAAO,CAAC;CACpD,IAAI,CAAC,QACH,OAAO;EAAE,QAAQ;EAAI,UAAU;EAAM,YAAY,KAAK,IAAI,IAAI;EAAS,SAAS;EAAM,YAAY;EAAO,UAAU;CAAM;CAC3H,MAAM,SAAS,CAAC,OAAO,QAAQ,OAAO,MAAM,CAAC,CAAC,QAAO,SAAQ,KAAK,KAAK,MAAM,EAAE,CAAC,CAAC,KAAI,SAAQ,KAAK,QAAQ,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI;CAC/H,QAAQ,UAAU,MAAM;CACxB,OAAO;EAAE;EAAQ,UAAU,OAAO;EAAU,YAAY,KAAK,IAAI,IAAI;EAAS,SAAS;EAAO,YAAY;EAAO,UAAU;CAAM;AACnI"}
|
|
1
|
+
{"version":3,"file":"workspace-exec-stream-Bzxczzu2.js","names":[],"sources":["../src/ipc/memory.ts","../src/ipc/remote-oauth.ts","../src/ipc/workspace-exec-stream.ts"],"sourcesContent":["/**\n * In-process transport: the host dispatcher in the window's own process, and\n * `IpcClient`s that call it directly instead of over a unix socket.\n *\n * For a window that runs its own host (TUI local mode on the shared engine):\n * the same dispatcher, method table and signal fan-out a daemon serves, with\n * no socket, no spawn and no second process. A caller sees what a socket\n * client would, so the daemon-mode code paths run on it unchanged:\n * - params, results and signals are JSON copies (as on the wire: a top-level\n * `undefined` result arrives as `null`, nested `undefined` fields are\n * dropped, and nothing is shared between the window and the host);\n * - a failed request rejects with `JsonRpcRemoteError` carrying the\n * dispatcher's code; a closed connection rejects with\n * `JsonRpcConnectionClosedError`, in-flight requests included;\n * - responses and signals arrive asynchronously, through one queue in the\n * order the host produced them (a socket reads them in write order);\n * - `close()` detaches quietly; the host shutting down calls `onClose`.\n *\n * Two socket behaviors are deliberately absent. Request deadlines exist on\n * the socket to recover from a wedged daemon by dropping the connection and\n * reconnecting; in-process there is nothing to reconnect to. And\n * `daemon.shutdown` from a client does not stop the host: its lifetime\n * belongs to the embedding window (`InProcessHost.close()`).\n */\nimport type { ChatHost } from '../chat/host/engine'\nimport type { IpcClient } from './client'\nimport type { HostPeer } from './daemon/dispatcher'\nimport type { DaemonEnvironment } from './daemon/environment'\nimport type { HelloResult, HostSignal } from './protocol'\nimport type { SignalListener } from './transports/client-core'\nimport { JsonRpcConnectionClosedError, JsonRpcRemoteError } from '../acp/json-rpc'\nimport { ZIDANE_VERSION } from '../version'\nimport { createHostDispatcher } from './daemon/dispatcher'\nimport { IPC_BUILD_ID } from './daemon/lifecycle'\nimport { IPC_PROTOCOL_VERSION } from './protocol'\n\n/** Options for {@link startInProcessHost}. */\nexport interface InProcessHostOptions {\n /** The host's environment, given the broadcast sink wired to client fan-out. */\n createEnvironment: (broadcast: (signal: HostSignal) => void) => DaemonEnvironment | Promise<DaemonEnvironment>\n /** Reported to clients as the host's `appVersion`. */\n appVersion?: string\n}\n\n/** Options for {@link InProcessHost.connect}. */\nexport interface InProcessClientOptions {\n /** Client version, surfaced in the host's diagnostics. */\n appVersion?: string\n /** Called when the host shuts down under a still-open client. */\n onClose?: () => void\n}\n\n/** A chat host running in the caller's process, with clients that call it directly. */\nexport interface InProcessHost {\n /** The chat host. */\n host: ChatHost\n /** A new client, already past the `ipc.hello` handshake. */\n connect: (options?: InProcessClientOptions) => Promise<IpcClient>\n /** Number of open clients. */\n peerCount: () => number\n /**\n * Shut the host down: refuse new work, abort and await every run, then\n * close the clients still open (their `onClose` fires). Idempotent.\n */\n close: () => Promise<void>\n}\n\n/** JSON round-trip: what crossing the socket does to a value. */\nfunction wire<T>(value: T): T {\n return value === undefined ? (null as T) : JSON.parse(JSON.stringify(value)) as T\n}\n\n/** One open client, from the host's side. */\ninterface Connection {\n peer: HostPeer\n /** The host is shutting down: reject what is pending, then tell the client. */\n hostClosed: () => void\n}\n\n/**\n * Starts a chat host in this process. Clients from `connect` behave like socket clients, without a socket or a second process.\n * @param options - Environment factory and version.\n * @returns The host handle.\n */\nexport async function startInProcessHost(options: InProcessHostOptions): Promise<InProcessHost> {\n const connections = new Set<Connection>()\n let closing: Promise<void> | null = null\n\n const dispatcher = await createHostDispatcher({\n createEnvironment: options.createEnvironment,\n appVersion: options.appVersion ?? 'dev',\n startedAt: Date.now(),\n identity: { kind: 'in-process' },\n lifecycle: {\n // Owned by the embedding window, see the module header.\n shutdown: () => undefined,\n // Clients are this build by construction: never a newer one to retire for.\n retireWhenIdle: () => undefined,\n isRetiring: () => false,\n },\n })\n\n async function connect(clientOptions: InProcessClientOptions = {}): Promise<IpcClient> {\n if (closing)\n throw new JsonRpcConnectionClosedError()\n const listeners = new Set<SignalListener>()\n const pending = new Set<(err: Error) => void>()\n let closed = false\n\n /*\n * Everything bound for the client goes through one FIFO drained in a\n * microtask: signals, and request outcomes once the dispatcher settles\n * them. A signal the host emits after answering a request is therefore\n * delivered after that answer, as a socket would read them.\n */\n const outbox: Array<() => void> = []\n let draining = false\n const deliver = (item: () => void): void => {\n outbox.push(item)\n if (draining)\n return\n draining = true\n queueMicrotask(() => {\n try {\n while (outbox.length > 0) {\n try {\n outbox.shift()!()\n }\n catch {\n // A throwing listener costs that one message, as on the socket\n // (the JSON-RPC reader catches it), never the connection.\n }\n }\n }\n finally {\n draining = false\n }\n })\n }\n\n const peer: HostPeer = {\n ready: false,\n watching: null,\n send(signal, seq, encoded) {\n if (closed)\n return\n const meta = seq === undefined ? undefined : { seq }\n let copy: HostSignal\n try {\n // The fan-out's one serialization, when it has one.\n const json = encoded ? encoded.json() : JSON.stringify(signal)\n if (json === undefined)\n return\n copy = JSON.parse(json) as HostSignal\n }\n catch {\n // Not JSON-able: the socket server drops it the same way, and a\n // bad signal must never break fan-out to the other clients.\n return\n }\n deliver(() => {\n if (closed)\n return\n for (const listener of listeners)\n listener(copy, meta)\n })\n },\n }\n\n const request = (method: string, params: unknown): Promise<unknown> => {\n if (closed)\n return Promise.reject(new JsonRpcConnectionClosedError())\n return new Promise((resolve, reject) => {\n pending.add(reject)\n const settle = (outcome: () => void): void => {\n deliver(() => {\n if (!pending.delete(reject))\n return\n outcome()\n })\n }\n dispatcher.handleRequest(method, params === undefined ? undefined : wire(params), peer).then(\n (result) => {\n settle(() => {\n // The reject is already out of `pending`: a throw here (a result\n // that is not JSON-able) must fail the request, not hang it.\n try {\n if (result.ok)\n resolve(wire(result.result))\n else\n reject(new JsonRpcRemoteError(result.message, result.code, result.data))\n }\n catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n }\n })\n },\n err => settle(() => reject(err)),\n )\n })\n }\n\n /** Stop delivering and fail what is in flight, as a socket closing does. */\n const shut = (): void => {\n closed = true\n listeners.clear()\n outbox.length = 0\n for (const reject of pending)\n reject(new JsonRpcConnectionClosedError())\n pending.clear()\n }\n\n const connection: Connection = {\n peer,\n hostClosed: () => {\n if (closed)\n return\n shut()\n clientOptions.onClose?.()\n },\n }\n\n dispatcher.addPeer(peer)\n connections.add(connection)\n const detach = (): void => {\n if (closed)\n return\n shut()\n connections.delete(connection)\n dispatcher.removePeer(peer)\n }\n\n let hello: HelloResult\n try {\n hello = await request('ipc.hello', {\n protocolVersion: IPC_PROTOCOL_VERSION,\n appVersion: clientOptions.appVersion ?? 'dev',\n pid: process.pid,\n buildId: IPC_BUILD_ID,\n codeVersion: ZIDANE_VERSION,\n }) as HelloResult\n }\n catch (err) {\n detach()\n throw err\n }\n\n return {\n hello,\n request: request as IpcClient['request'],\n notify: (method, params) => {\n if (closed)\n return\n const copy = params === undefined ? undefined : wire(params)\n // Off the caller's stack, as a socket write would be.\n queueMicrotask(() => {\n if (!closed)\n void dispatcher.handleNotification(method, copy, peer)\n })\n },\n onSignal: (listener) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n close: detach,\n }\n }\n\n function close(): Promise<void> {\n // Same order as the socket server's shutdown: drain the host, close\n // every connection, then forget the peers without per-peer cleanup.\n closing ??= (async () => {\n await dispatcher.host.drain()\n for (const connection of connections)\n connection.hostClosed()\n connections.clear()\n dispatcher.detachAll()\n })()\n return closing\n }\n\n return {\n host: dispatcher.host,\n connect,\n peerCount: () => dispatcher.peerCount(),\n close,\n }\n}\n","/**\n * The client half of a remote OAuth sign-in (`auth.oauth.*`): the host runs\n * the login and keeps its secrets; this machine has the browser. For an MCP\n * server it opens a loopback listener first (any free port) and hands its\n * URI to the host; for a provider it listens on the provider's fixed\n * `redirect_uri` once the host names it (a busy port falls back to pasting\n * the address the browser lands on). It opens the URL, forwards the code\n * (or what the user pasted) to the host, and resolves with the host's\n * outcome, which never carries a token. Device and polling flows just wait.\n *\n * The provider's own questions (a choice between flows, a free-text\n * prompt: `HostOAuthStep`) come back from `begin` / `complete`; the UI\n * answers them (`select` / `prompt`; a prompt falls back to `paste`). A UI\n * that can't answers nothing: the sign-in ends with why, never a default.\n */\nimport type { McpOAuthCallbackPageRenderer, OAuthCallbackHandle } from '../mcp/oauth-callback'\nimport type { HostOAuthBegin, HostOAuthComplete, HostOAuthStep } from './protocol'\nimport type { IpcClient } from './transports/client-core'\nimport { startOAuthCallback } from '../mcp/oauth-callback'\n\n/** What to sign in to: a provider by key, or an MCP server by name. */\nexport type RemoteOAuthTarget = { provider: string } | { mcpServer: string, cwd?: string, kind?: string }\n\nexport interface RemoteOAuthStart extends HostOAuthBegin {\n /** This machine catches the redirect (else: paste, or wait for a device / polling flow). */\n listening: boolean\n /** Why it doesn't (a busy port). */\n listenError?: string\n}\n\n/** Hooks a window supplies to {@link runRemoteOAuth}: open the browser, take pasted input, answer the provider's questions, and abort. */\nexport interface RemoteOAuthUi {\n /** Open the browser here (best effort; the URL is shown too). */\n openUrl: (url: string) => void | Promise<void>\n /** The flow started: the URL, the mode, a device code, whether the loopback listens. */\n onStart?: (start: RemoteOAuthStart) => void\n /** What the user pastes (a code or the address the browser landed on); offered in every mode. */\n paste?: () => Promise<string>\n /** Aborts the sign-in. The run rejects with \"OAuth login aborted\". */\n signal?: AbortSignal\n /** The page the browser lands on at the loopback. */\n renderPage?: McpOAuthCallbackPageRenderer\n /** The provider asks the user to choose (its option id; `undefined` cancels). */\n select?: (step: Extract<HostOAuthStep, { kind: 'select' }>) => Promise<string | undefined>\n /** The provider asks the user a question (default: {@link paste}). */\n prompt?: (step: Extract<HostOAuthStep, { kind: 'prompt' }>) => Promise<string>\n}\n\n/** The UI's answer to `step`, or why it can't give one. */\nasync function answerStep(ui: RemoteOAuthUi, step: HostOAuthStep): Promise<string | undefined> {\n if (step.kind === 'select' && ui.select)\n return ui.select(step)\n if (step.kind === 'prompt' && (ui.prompt || ui.paste))\n return ui.prompt ? ui.prompt(step) : ui.paste!()\n throw new Error(`The sign-in asks \"${step.message}\", and this window can't answer it here: sign in on the server machine or use an API key.`)\n}\n\nconst abortError = (): Error => new Error('OAuth login aborted')\n\nfunction whenAborted(signal: AbortSignal | undefined): Promise<never> {\n return new Promise<never>((_, reject) => {\n if (signal?.aborted)\n reject(abortError())\n signal?.addEventListener('abort', () => reject(abortError()), { once: true })\n })\n}\n\n/**\n * Runs the client half of a remote OAuth sign-in against a host.\n * Starts the flow on the host, opens the URL through the UI, forwards the code from the loopback listener or from `paste`, and resolves with the host's outcome, which never carries a token. Rejects when aborted or when the UI cannot answer a question the provider asks.\n * @param client - Client used for the `auth.oauth.*` requests.\n * @param target - Provider or MCP server to sign in to.\n * @param ui - Browser, prompt and abort hooks.\n * @returns The sign-in outcome: the provider status or the MCP server name.\n */\nexport async function runRemoteOAuth(\n client: Pick<IpcClient, 'request'>,\n target: RemoteOAuthTarget,\n ui: RemoteOAuthUi,\n): Promise<HostOAuthComplete> {\n const { signal } = ui\n signal?.throwIfAborted()\n const isMcp = 'mcpServer' in target\n let loopback: OAuthCallbackHandle | undefined\n const listenOptions = { ...(signal ? { signal } : {}), ...(ui.renderPage ? { renderPage: ui.renderPage } : {}), provider: isMcp ? target.mcpServer : target.provider }\n if (isMcp)\n loopback = await startOAuthCallback({ ...listenOptions, port: 0 })\n let begin: HostOAuthBegin | undefined\n try {\n begin = await client.request('auth.oauth.begin', isMcp\n ? { mcpServer: target.mcpServer, ...(target.cwd !== undefined ? { cwd: target.cwd } : {}), ...(target.kind !== undefined ? { kind: target.kind } : {}), redirectUri: loopback!.redirectUri }\n : { provider: target.provider })\n const flowId = begin.flowId\n const respond = async (step: HostOAuthStep): Promise<HostOAuthBegin> => {\n const value = await answerStep(ui, step)\n signal?.throwIfAborted()\n return client.request('auth.oauth.respond', { flowId, stepId: step.id, ...(value !== undefined ? { value } : {}) })\n }\n // Questions before the URL.\n while (begin.step)\n begin = await respond(begin.step)\n let listenError: string | undefined\n // The host pinned another redirect (an MCP server's pre-registered\n // client, `oauth.callbackPort`): listen there instead.\n if (loopback && begin.redirectUri && begin.redirectUri !== loopback.redirectUri) {\n loopback.promise.catch(() => undefined) // closing rejects it; nothing waits on it\n await loopback.close()\n loopback = undefined\n }\n if (!loopback && begin.mode === 'loopback' && begin.redirectUri) {\n const redirect = new URL(begin.redirectUri)\n try {\n loopback = await startOAuthCallback({ ...listenOptions, port: Number(redirect.port), path: redirect.pathname })\n }\n catch (err) {\n listenError = `Port ${redirect.port} is busy on this computer (${err instanceof Error ? err.message : String(err)}): paste the address the browser lands on instead.`\n }\n }\n ui.onStart?.({ ...begin, listening: loopback !== undefined, ...(listenError ? { listenError } : {}) })\n await ui.openUrl(begin.url)\n\n /** Deliver (once), then wait; a question after the URL is answered and the wait goes on. */\n const complete = async (params: { code?: string, state?: string, input?: string }): Promise<HostOAuthComplete> => {\n let outcome = await client.request('auth.oauth.complete', { flowId, ...params })\n while (outcome.step) {\n await respond(outcome.step)\n outcome = await client.request('auth.oauth.complete', { flowId })\n }\n return outcome\n }\n const ways: Promise<HostOAuthComplete>[] = [whenAborted(signal)]\n if (loopback) {\n const handle = loopback\n ways.push(handle.promise.then(({ code, state }) => complete({ code, ...(state !== undefined ? { state } : {}) })))\n }\n if (ui.paste)\n ways.push(ui.paste().then(input => complete({ input })))\n // Device and polling flows finish on the host's side: wait for it.\n if (begin.mode !== 'loopback')\n ways.push(complete({}))\n for (const way of ways)\n way.catch(() => undefined)\n return await Promise.race(ways)\n }\n catch (err) {\n if (begin)\n void client.request('auth.oauth.cancel', { flowId: begin.flowId }).catch(() => undefined)\n throw err\n }\n finally {\n await loopback?.close()\n }\n}\n","/**\n * The client half of a streamed workspace command (`!` on a remote host):\n * starts `sessions.workspaceExecStream`, gathers its `workspace:exec:*`\n * signals (which may beat the reply) into one output in arrival order, and\n * cancels on the host when `signal` aborts. The result has the shape of a\n * local `!` run (`BashRunResult`), so both format and persist alike. A host\n * without the method gets the buffered `sessions.workspaceExec`, where an\n * abort stops waiting, not the command.\n */\nimport type { HostSignal } from './protocol'\nimport type { IpcClient } from './transports/client-core'\nimport { isMethodNotFound } from '../chat/hosts/connection'\n\n/** Outcome of {@link runWorkspaceExecStream}, shaped like a local `!` run. */\nexport interface WorkspaceExecStreamResult {\n /** stdout and stderr in arrival order (the buffered fallback: stdout, then stderr). */\n output: string\n /** `null` when the host killed it (cancel, output cap). */\n exitCode: number | null\n /** Wall-clock time of the run in milliseconds. */\n durationMs: number\n /** Cancelled through `signal`. */\n aborted: boolean\n /** The host's output cap was reached and the process stopped. */\n overflowed: boolean\n /** `false`: an older host answered with the buffered exec. */\n streamed: boolean\n}\n\n/** Options for {@link runWorkspaceExecStream}. */\nexport interface WorkspaceExecStreamOptions {\n /** Cancels the command on the host when aborted. */\n signal?: AbortSignal\n /** The whole output so far, on every chunk. */\n onChunk?: (accumulated: string) => void\n /** The buffered fallback's bound. Default 600 s. */\n fallbackTimeoutSeconds?: number\n /** How long to wait for the host's exit after a cancel before giving up on it. Default 10 s. */\n cancelGraceMs?: number\n}\n\ntype ExecSignal = Extract<HostSignal, { kind: 'workspace:exec:data' | 'workspace:exec:exit' }>\n\n/**\n * Runs a command in a session's workspace on a host and streams its output.\n * The result has the shape of a local `!` run. A host without `sessions.workspaceExecStream` gets the buffered `sessions.workspaceExec` instead, and then an abort stops the wait, not the command.\n * @param client - Client used for requests and signals.\n * @param params - Session, command and optional working directory.\n * @param options - Abort signal, chunk callback and timeouts.\n * @returns The output, exit code and flags describing how it ended.\n */\nexport async function runWorkspaceExecStream(\n client: Pick<IpcClient, 'request' | 'onSignal'>,\n params: { sessionId: string, command: string, cwd?: string },\n options: WorkspaceExecStreamOptions = {},\n): Promise<WorkspaceExecStreamResult> {\n const { signal } = options\n const started = Date.now()\n signal?.throwIfAborted()\n\n let execId: string | null = null\n /** Signals that arrived before the reply named the exec. */\n const early: ExecSignal[] = []\n let output = ''\n let finish: (exit: Extract<ExecSignal, { kind: 'workspace:exec:exit' }> | null) => void = () => undefined\n const exited = new Promise<Extract<ExecSignal, { kind: 'workspace:exec:exit' }> | null>((resolve) => {\n finish = resolve\n })\n const apply = (event: ExecSignal): void => {\n if (event.kind === 'workspace:exec:data') {\n output += event.chunk\n options.onChunk?.(output)\n return\n }\n finish(event)\n }\n const unsubscribe = client.onSignal((event) => {\n if (event.kind !== 'workspace:exec:data' && event.kind !== 'workspace:exec:exit')\n return\n if (execId === null)\n early.push(event)\n else if (event.execId === execId)\n apply(event)\n })\n\n try {\n try {\n execId = (await client.request('sessions.workspaceExecStream', { sessionId: params.sessionId, command: params.command, ...(params.cwd !== undefined ? { cwd: params.cwd } : {}) })).execId\n }\n catch (err) {\n if (!isMethodNotFound(err))\n throw err\n return await buffered(client, params, options, started)\n }\n for (const event of early.splice(0)) {\n if (event.execId === execId)\n apply(event)\n }\n\n const id = execId\n const onAbort = (): void => {\n void client.request('sessions.workspaceExecCancel', { execId: id }).catch(() => undefined)\n // The exit normally follows the kill; a connection lost meanwhile must not hang the caller.\n setTimeout(finish, options.cancelGraceMs ?? 10_000, null).unref?.()\n }\n signal?.addEventListener('abort', onAbort, { once: true })\n if (signal?.aborted)\n onAbort()\n let exit: Awaited<typeof exited>\n try {\n exit = await exited\n }\n finally {\n signal?.removeEventListener('abort', onAbort)\n }\n return {\n output,\n exitCode: exit === null || exit.signal !== null ? null : exit.code,\n durationMs: Date.now() - started,\n aborted: signal?.aborted === true,\n overflowed: exit?.truncated === true,\n streamed: true,\n }\n }\n finally {\n unsubscribe()\n }\n}\n\n/** An older host: one buffered exec; `signal` stops waiting for it. */\nasync function buffered(\n client: Pick<IpcClient, 'request'>,\n params: { sessionId: string, command: string, cwd?: string },\n options: WorkspaceExecStreamOptions,\n started: number,\n): Promise<WorkspaceExecStreamResult> {\n const { signal } = options\n const request = client.request('sessions.workspaceExec', {\n sessionId: params.sessionId,\n command: params.command,\n ...(params.cwd !== undefined ? { cwd: params.cwd } : {}),\n timeoutSeconds: options.fallbackTimeoutSeconds ?? 600,\n })\n const stopped = new Promise<null>((resolve) => {\n if (signal?.aborted)\n resolve(null)\n signal?.addEventListener('abort', () => resolve(null), { once: true })\n })\n request.catch(() => undefined)\n const result = await Promise.race([request, stopped])\n if (!result)\n return { output: '', exitCode: null, durationMs: Date.now() - started, aborted: true, overflowed: false, streamed: false }\n const output = [result.stdout, result.stderr].filter(part => part.trim() !== '').map(part => part.replace(/\\n$/, '')).join('\\n')\n options.onChunk?.(output)\n return { output, exitCode: result.exitCode, durationMs: Date.now() - started, aborted: false, overflowed: false, streamed: false }\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.js";
|
|
2
2
|
import { t as HostIdentityError } from "./connection-BaHd3T-D.js";
|
|
3
|
-
import { i as IpcVersionMismatchError } from "./settings-keys-
|
|
3
|
+
import { i as IpcVersionMismatchError } from "./settings-keys-CnrJgT1T.js";
|
|
4
4
|
import { u as hostRefOf } from "./protocol-Do_i6Ras.js";
|
|
5
5
|
import { t as keepHostConnected } from "./connection-loop-DSCehV3W.js";
|
|
6
6
|
import { n as displayHostUrl } from "./url-D4vG7lzO.js";
|
|
7
7
|
import { i as probeUnsavedHost, t as createHostRegistry } from "./registry-DxPP_tAt.js";
|
|
8
|
-
import { a as hostCaBundle, r as connectWsClient, t as HostAuthError } from "./ws-client-
|
|
8
|
+
import { a as hostCaBundle, r as connectWsClient, t as HostAuthError } from "./ws-client-Dond8vT3.js";
|
|
9
9
|
//#region src/ipc/hosts/probe.ts
|
|
10
10
|
/** Bound on one probe: the upgrade, the handshake and `daemon.info`. */
|
|
11
11
|
const HOST_PROBE_TIMEOUT_MS = 1e4;
|
|
@@ -281,4 +281,4 @@ function startWsSavedHosts(options) {
|
|
|
281
281
|
//#endregion
|
|
282
282
|
export { MissingHostTokenError as a, createWsHostRegistry as c, ws_exports as i, probeWsHost as l, probeUnsavedWsHost as n, startSavedHosts as o, startWsSavedHosts as r, HOST_PROBE_TIMEOUT_MS as s, classifyWsFailure as t };
|
|
283
283
|
|
|
284
|
-
//# sourceMappingURL=ws-
|
|
284
|
+
//# sourceMappingURL=ws-CrD-4Mx6.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-B2HB87nz.js","names":[],"sources":["../src/ipc/hosts/probe.ts","../src/ipc/hosts/saved-hosts.ts","../src/ipc/hosts/ws.ts"],"sourcesContent":["/**\n * The host registry's connection (`src/chat/hosts`): a WebSocket probe that\n * says who answers at a URL with a token, and the registry wired to it.\n */\nimport type { HostErrorCode, HostProbeResult, HostRegistry } from '../../chat/hosts'\nimport { createHostRegistry, displayHostUrl } from '../../chat/hosts'\nimport { hostRefOf } from '../protocol'\nimport { IpcVersionMismatchError } from '../transports/client-core'\nimport { hostCaBundle } from '../transports/tls-trust'\nimport { connectWsClient, HostAuthError } from '../transports/ws-client'\n\n/** Bound on one probe: the upgrade, the handshake and `daemon.info`. */\nexport const HOST_PROBE_TIMEOUT_MS = 10_000\n\nconst TLS_FAILURE = /certificate|self[- ]signed|\\btls\\b|\\bssl\\b|UNABLE_TO_VERIFY|CERT_|ERR_TLS/i\n\nfunction classify(err: unknown): { error: HostErrorCode, message: string } {\n if (err instanceof HostAuthError) {\n return err.status === 401\n ? { error: 'auth', message: 'The host refused the token (401). Create a new one on the server: zidane server token create --name <device>.' }\n : { error: 'auth', message: err.message }\n }\n if (err instanceof IpcVersionMismatchError)\n return { error: 'protocol', message: err.message }\n const message = err instanceof Error ? err.message : String(err)\n if (TLS_FAILURE.test(message))\n return { error: 'tls', message: `TLS failed: ${message}` }\n return { error: 'unreachable', message }\n}\n\n/**\n * A failed WebSocket says nothing about why. For `wss://`, ask the same\n * address over HTTPS: a certificate or handshake failure there is a TLS\n * problem, anything else stays \"unreachable\".\n */\nasync function tlsFailure(url: string, signal: AbortSignal): Promise<string | null> {\n if (!url.startsWith('wss:'))\n return null\n try {\n // No token, and no redirect followed: only the TLS handshake with this origin matters.\n // The same CAs as the socket (Bun takes them per request; Node uses its process default).\n const ca = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' ? hostCaBundle() : null\n const response = await fetch(`https:${url.slice('wss:'.length)}`, { redirect: 'manual', signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]), ...(ca ? { tls: { ca } } : {}) } as RequestInit)\n void response.body?.cancel().catch(() => {})\n return null\n }\n catch (err) {\n const e = err as { code?: unknown, message?: unknown }\n const detail = `${typeof e.code === 'string' ? e.code : ''} ${typeof e.message === 'string' ? e.message : ''}`\n return TLS_FAILURE.test(detail) ? `TLS failed: ${String(e.message ?? e.code)}` : null\n }\n}\n\n/**\n * Connect, read the host's identity and `daemon.info`, disconnect. Never throws.\n * @param target - URL, token and an optional timeout in milliseconds. The timeout defaults to {@link HOST_PROBE_TIMEOUT_MS}.\n * @returns Who answered and how fast, or a classified failure.\n */\nexport async function probeWsHost(target: { url: string, token: string, timeoutMs?: number }): Promise<HostProbeResult> {\n const started = performance.now()\n const signal = AbortSignal.timeout(target.timeoutMs ?? HOST_PROBE_TIMEOUT_MS)\n let client: Awaited<ReturnType<typeof connectWsClient>> | undefined\n try {\n client = await connectWsClient({ url: target.url, token: target.token, heartbeatMs: 0, signal })\n const hostRef = hostRefOf(client.hello)\n if (!hostRef)\n return { ok: false, error: 'protocol', message: `${displayHostUrl(target.url)} runs a zidane too old to say which host it is. Upgrade the server.` }\n const info = await client.request('daemon.info', {})\n const serverVersion = info.codeVersion ?? info.appVersion\n return { ok: true, hostRef, ...(serverVersion ? { serverVersion } : {}), latencyMs: Math.round(performance.now() - started) }\n }\n catch (err) {\n if (signal.aborted && !(err instanceof HostAuthError))\n return { ok: false, error: 'unreachable', message: `No answer from ${displayHostUrl(target.url)} within ${target.timeoutMs ?? HOST_PROBE_TIMEOUT_MS} ms.` }\n const classified = classify(err)\n if (classified.error === 'unreachable') {\n const tls = await tlsFailure(target.url, signal).catch(() => null)\n if (tls)\n return { ok: false, error: 'tls', message: tls }\n }\n return { ok: false, ...classified }\n }\n finally {\n client?.close()\n }\n}\n\n/**\n * The registry in `dataDir`, connecting over WebSocket.\n * @param options - Where the registry keeps its data.\n * @returns The host registry.\n */\nexport function createWsHostRegistry(options: { dataDir: string }): HostRegistry {\n return createHostRegistry({ dataDir: options.dataDir, probe: probeWsHost })\n}\n","/**\n * A client's saved servers, kept in its host directory: one directory entry\n * and one app-lifetime connection (`connection-loop`) per server in the\n * registry, in step with it. Shared by the TUI and the GUI main process,\n * which each supply their directory (`SavedHostsDirectory`) and how to dial.\n *\n * Saving a server that is already saved (same host id: a new token, a new\n * URL) reconfigures its entry IN PLACE and reconnects: the entry is never\n * removed, so the sessions routed to it stay routed. Only a server leaving\n * the registry (removed here or by another window) or `stop()` removes an\n * entry, and the directory decides what that means for an open session.\n *\n * Every connection is checked against the saved entry's id (the server's\n * `hostId` when it was added): another host answering at the URL is closed\n * at once, reported as `identity` and not retried, so nothing it says is\n * recorded under the saved id. Dialers may check first (`expectHostId`).\n *\n * No WebSocket import: the WebSocket wiring is `./ws.ts`.\n */\nimport type { HostEntry, HostProbeResult, HostRegistry, HostTestResult } from '../../chat/hosts'\nimport type { HostSignal } from '../protocol'\nimport type { IpcClient } from '../transports/client-core'\nimport type { HostConnectionLoop, HostConnectionPatch, HostFailureKind } from './connection-loop'\nimport { displayHostUrl, HostIdentityError } from '../../chat/hosts'\nimport { hostRefOf } from '../protocol'\nimport { keepHostConnected } from './connection-loop'\n\n/** The client's host directory, as the saved servers see it. Keys are host ids. */\nexport interface SavedHostsDirectory {\n /** The saved servers it holds now, with their URL (a `--host` server excluded). */\n saved: () => { key: string, url: string }[]\n /** Add the entry, or reconfigure the one under its key in place (status `connecting`). */\n upsert: (entry: HostEntry, token: string | undefined) => void\n /** Applies a connection patch, a new name or a server version to the entry. */\n update: (key: string, patch: HostConnectionPatch & { name?: string, serverVersion?: string }) => void\n /** The server left the registry, or the saved servers stopped. */\n remove: (key: string) => void\n}\n\n/** No token is saved for this server (removed by hand, or another window removed it). */\nexport class MissingHostTokenError extends Error {\n constructor() {\n super('No token saved for this server: add it again.')\n this.name = 'MissingHostTokenError'\n }\n}\n\n/** Options for {@link startSavedHosts}: the registry, the host directory, the connect function and callbacks. */\nexport interface SavedHostsOptions {\n /** The registry of saved servers. */\n registry: HostRegistry\n /** The client's host directory the entries are kept in. */\n directory: SavedHostsDirectory\n /** One connect attempt to a saved server, with its token read fresh. */\n connect: (entry: HostEntry, token: string, options: { signal: AbortSignal, onClose: () => void }) => Promise<IpcClient>\n /** `auth`: refused for good. A missing token is always `auth`, another host answering always `identity`. */\n classify?: (err: unknown) => HostFailureKind\n /** The add form's test of an unsaved URL + token (`probeUnsavedHost`). */\n probeUnsaved?: (target: { url: string, token: string }) => Promise<HostProbeResult>\n /** Saved entries the directory already reaches another way (the TUI's `--host` server). */\n skip?: (entry: HostEntry) => boolean\n /** Called with every signal of a connected saved server. */\n onSignal?: (key: string, signal: HostSignal) => void\n /** Called when a saved server connects, also after a reconnect. */\n onConnected?: (key: string, client: IpcClient) => void\n /** Called when a saved server's connection drops. */\n onDisconnected?: (key: string) => void\n /**\n * Lower backoff bound in milliseconds.\n * @default 1000\n */\n minDelayMs?: number\n /**\n * Upper backoff bound in milliseconds.\n * @default 30000\n */\n maxDelayMs?: number\n}\n\n/** Handle returned by {@link startSavedHosts} to list, add, remove, rename, test and retry saved servers. */\nexport interface SavedHostsController {\n /** The saved hosts, read fresh (other windows share the file). */\n list: () => HostEntry[]\n /** Verify, save, connect (in place when already saved). Throws a `HostRegistryError`. */\n add: (input: { url: string, token: string, name?: string }) => Promise<HostEntry>\n /** Removes a server from the registry and its entry from the directory. Returns whether it existed. */\n remove: (id: string) => boolean\n /** Renames a saved server and returns the updated entry. */\n rename: (id: string, name: string) => HostEntry\n /** A saved server (by id), or an unsaved URL + token (the add form's test). */\n test: (target: string | { id: string } | { url: string, token: string }) => Promise<HostTestResult>\n /** Reconnect now (after an outage or a token fix). */\n retry: (id: string) => void\n /** Reconnect every saved server that is not connected now, its backoff reset (the computer woke up). */\n retryAll: () => void\n /** Disconnects every saved server and removes their directory entries. The registry is not changed. */\n stop: () => void\n}\n\n/**\n * Starts the connections of a client's saved servers and keeps the directory in step with the registry.\n * Each saved server gets a directory entry and a connection loop. A connection whose host id differs from the saved one is closed and reported as `identity`.\n * @param options - Registry, directory and how to dial.\n * @returns A controller over the saved servers.\n */\nexport function startSavedHosts(options: SavedHostsOptions): SavedHostsController {\n const { registry, directory, skip = () => false } = options\n const classify = (err: unknown): HostFailureKind => err instanceof MissingHostTokenError ? 'auth' : err instanceof HostIdentityError ? 'identity' : (options.classify?.(err) ?? 'retry')\n\n /** The saved server, or nobody: a connection from another host is closed before anyone sees it. */\n const dial = async (entry: HostEntry, token: string, attempt: { signal: AbortSignal, onClose: () => void }): Promise<IpcClient> => {\n const client = await options.connect(entry, token, attempt)\n const ref = hostRefOf(client.hello)\n if (ref?.id === entry.id)\n return client\n client.close()\n throw new HostIdentityError(displayHostUrl(entry.url), ref?.name)\n }\n const loops = new Map<string, HostConnectionLoop>()\n\n const disconnect = (key: string): void => {\n loops.get(key)?.stop()\n loops.delete(key)\n }\n\n /** (Re)connect `entry` with the token saved now, its directory entry kept or created in place. */\n const connect = (entry: HostEntry): void => {\n const key = entry.id\n disconnect(key)\n directory.upsert(entry, registry.token(key))\n loops.set(key, keepHostConnected({\n connect: (attempt) => {\n const token = registry.token(key)\n return token ? dial(entry, token, attempt) : Promise.reject(new MissingHostTokenError())\n },\n update: patch => directory.update(key, patch),\n classify,\n ...(options.onSignal ? { onSignal: (signal: HostSignal) => options.onSignal!(key, signal) } : {}),\n ...(options.onConnected ? { onConnected: (client: IpcClient) => options.onConnected!(key, client) } : {}),\n ...(options.onDisconnected ? { onDisconnected: () => options.onDisconnected!(key) } : {}),\n ...(options.minDelayMs !== undefined ? { minDelayMs: options.minDelayMs } : {}),\n ...(options.maxDelayMs !== undefined ? { maxDelayMs: options.maxDelayMs } : {}),\n }))\n }\n\n /** Bring the directory in line with the saved hosts. */\n const sync = (): void => {\n const entries = registry.list().filter(entry => !skip(entry))\n const saved = new Set(entries.map(entry => entry.id))\n const known = new Map(directory.saved().map(host => [host.key, host.url]))\n for (const key of known.keys()) {\n if (!saved.has(key)) {\n disconnect(key)\n directory.remove(key)\n }\n }\n for (const entry of entries) {\n if (known.get(entry.id) !== entry.url)\n connect(entry)\n else\n directory.update(entry.id, { name: entry.name, ...(entry.serverVersion ? { serverVersion: entry.serverVersion } : {}) })\n }\n }\n\n sync()\n return {\n list: () => registry.list(),\n add: async (input) => {\n const entry = await registry.add(input)\n // Saved again (a new token or URL): reconnect with them, the entry kept.\n if (!skip(entry))\n connect(entry)\n sync()\n return entry\n },\n remove: (id) => {\n const removed = registry.remove(id)\n sync()\n return removed\n },\n rename: (id, name) => {\n const entry = registry.rename(id, name)\n sync()\n return entry\n },\n test: async (target) => {\n if (typeof target === 'object' && !('id' in target)) {\n if (!options.probeUnsaved)\n throw new Error('Testing an unsaved host is not available here.')\n return options.probeUnsaved(target)\n }\n const id = typeof target === 'string' ? target : target.id\n const result = await registry.test(id)\n if (result.ok) {\n directory.update(id, { serverName: result.hostRef.name, ...(result.serverVersion ? { serverVersion: result.serverVersion } : {}) })\n loops.get(id)?.retry()\n }\n return result\n },\n retry: id => loops.get(id)?.retry(),\n retryAll: () => {\n for (const loop of loops.values())\n loop.retry()\n },\n stop: () => {\n for (const key of [...loops.keys()]) {\n disconnect(key)\n directory.remove(key)\n }\n },\n }\n}\n","/**\n * Saved servers over WebSocket: the registry in `dataDir`, the add form's\n * unsaved test, the refusal classification and (by default) the dial, wired\n * for `startSavedHosts`. Loaded behind each client's remote-hosts gate.\n */\nimport type { HostProbeResult } from '../../chat/hosts'\nimport type { HostFailureKind } from './connection-loop'\nimport type { SavedHostsController, SavedHostsOptions } from './saved-hosts'\nimport { HostIdentityError, probeUnsavedHost } from '../../chat/hosts'\nimport { IpcVersionMismatchError } from '../transports/client-core'\nimport { connectWsClient, HostAuthError } from '../transports/ws-client'\nimport { createWsHostRegistry, probeWsHost } from './probe'\nimport { startSavedHosts } from './saved-hosts'\n\n/**\n * A refused token stays refused until the user changes something: no retry\n * loop. Another protocol version stays until one side is upgraded: a slow re-check.\n * @param err - The error a connect attempt failed with.\n * @returns `auth` for a refused token, `identity` for another host answering, `protocol` for a version mismatch, `retry` otherwise.\n */\nexport function classifyWsFailure(err: unknown): HostFailureKind {\n if (err instanceof HostAuthError)\n return 'auth'\n if (err instanceof HostIdentityError)\n return 'identity'\n return err instanceof IpcVersionMismatchError ? 'protocol' : 'retry'\n}\n\n/**\n * The add form's \"test\": the registry's own checks and probe over WebSocket, saving nothing.\n * @param target - URL and token to try.\n * @returns The probe result.\n */\nexport function probeUnsavedWsHost(target: { url: string, token: string }): Promise<HostProbeResult> {\n return probeUnsavedHost(probeWsHost, target)\n}\n\n/**\n * Options for {@link startWsSavedHosts}: {@link SavedHostsOptions} with the registry, dialer and classifier filled in.\n * `dataDir` holds the saved-hosts registry file.\n */\nexport type WsSavedHostsOptions = Omit<SavedHostsOptions, 'registry' | 'connect' | 'classify' | 'probeUnsaved'> & {\n dataDir: string\n /** Client version in the server's diagnostics (default dial only). */\n appVersion?: string\n /** Dial another way (the TUI goes through its directory entry's transport). Default: `connectWsClient`. */\n connect?: SavedHostsOptions['connect']\n registry?: SavedHostsOptions['registry']\n}\n\n/**\n * Keeps the saved servers of a client connected over WebSocket.\n * Uses the registry in `dataDir` unless `registry` is given, and dials with {@link connectWsClient} unless `connect` is given.\n * @param options - Data directory, host directory and callbacks.\n * @returns A controller to list, add, remove, test and retry hosts, and to stop them all.\n */\nexport function startWsSavedHosts(options: WsSavedHostsOptions): SavedHostsController {\n const { dataDir, appVersion, connect, registry, ...rest } = options\n return startSavedHosts({\n ...rest,\n registry: registry ?? createWsHostRegistry({ dataDir }),\n classify: classifyWsFailure,\n probeUnsaved: probeUnsavedWsHost,\n connect: connect ?? ((entry, token, attempt) => connectWsClient({\n url: entry.url,\n token,\n expectHostId: entry.id,\n ...(appVersion ? { appVersion } : {}),\n signal: attempt.signal,\n onClose: attempt.onClose,\n })),\n })\n}\n"],"mappings":";;;;;;;;;;AAYA,MAAa,wBAAwB;AAErC,MAAM,cAAc;AAEpB,SAAS,SAAS,KAAyD;CACzE,IAAI,eAAe,eACjB,OAAO,IAAI,WAAW,MAClB;EAAE,OAAO;EAAQ,SAAS;CAAgH,IAC1I;EAAE,OAAO;EAAQ,SAAS,IAAI;CAAQ;CAE5C,IAAI,eAAe,yBACjB,OAAO;EAAE,OAAO;EAAY,SAAS,IAAI;CAAQ;CACnD,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,IAAI,YAAY,KAAK,OAAO,GAC1B,OAAO;EAAE,OAAO;EAAO,SAAS,eAAe;CAAU;CAC3D,OAAO;EAAE,OAAO;EAAe;CAAQ;AACzC;;;;;;AAOA,eAAe,WAAW,KAAa,QAA6C;CAClF,IAAI,CAAC,IAAI,WAAW,MAAM,GACxB,OAAO;CACT,IAAI;EAGF,MAAM,KAAK,OAAQ,WAAiC,QAAQ,cAAc,aAAa,IAAI;EAE3F,CAAK,MADkB,MAAM,SAAS,IAAI,MAAM,CAAa,KAAK;GAAE,UAAU;GAAU,QAAQ,YAAY,IAAI,CAAC,QAAQ,YAAY,QAAQ,GAAI,CAAC,CAAC;GAAG,GAAI,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC;EAAG,CAAgB,EAAA,CACvL,MAAM,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;EAC3C,OAAO;CACT,SACO,KAAK;EACV,MAAM,IAAI;EACV,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO,GAAG,GAAG,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU;EAC1G,OAAO,YAAY,KAAK,MAAM,IAAI,eAAe,OAAO,EAAE,WAAW,EAAE,IAAI,MAAM;CACnF;AACF;;;;;;AAOA,eAAsB,YAAY,QAAsF;CACtH,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,SAAS,YAAY,QAAQ,OAAO,aAAA,GAAkC;CAC5E,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,gBAAgB;GAAE,KAAK,OAAO;GAAK,OAAO,OAAO;GAAO,aAAa;GAAG;EAAO,CAAC;EAC/F,MAAM,UAAU,UAAU,OAAO,KAAK;EACtC,IAAI,CAAC,SACH,OAAO;GAAE,IAAI;GAAO,OAAO;GAAY,SAAS,GAAG,eAAe,OAAO,GAAG,EAAE;EAAqE;EACrJ,MAAM,OAAO,MAAM,OAAO,QAAQ,eAAe,CAAC,CAAC;EACnD,MAAM,gBAAgB,KAAK,eAAe,KAAK;EAC/C,OAAO;GAAE,IAAI;GAAM;GAAS,GAAI,gBAAgB,EAAE,cAAc,IAAI,CAAC;GAAI,WAAW,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;EAAE;CAC9H,SACO,KAAK;EACV,IAAI,OAAO,WAAW,EAAE,eAAe,gBACrC,OAAO;GAAE,IAAI;GAAO,OAAO;GAAe,SAAS,kBAAkB,eAAe,OAAO,GAAG,EAAE,UAAU,OAAO,aAAA,IAAmC;EAAM;EAC5J,MAAM,aAAa,SAAS,GAAG;EAC/B,IAAI,WAAW,UAAU,eAAe;GACtC,MAAM,MAAM,MAAM,WAAW,OAAO,KAAK,MAAM,CAAC,CAAC,YAAY,IAAI;GACjE,IAAI,KACF,OAAO;IAAE,IAAI;IAAO,OAAO;IAAO,SAAS;GAAI;EACnD;EACA,OAAO;GAAE,IAAI;GAAO,GAAG;EAAW;CACpC,UACQ;EACN,QAAQ,MAAM;CAChB;AACF;;;;;;AAOA,SAAgB,qBAAqB,SAA4C;CAC/E,OAAO,mBAAmB;EAAE,SAAS,QAAQ;EAAS,OAAO;CAAY,CAAC;AAC5E;;;;ACtDA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,cAAc;EACZ,MAAM,+CAA+C;EACrD,KAAK,OAAO;CACd;AACF;;;;;;;AA4DA,SAAgB,gBAAgB,SAAkD;CAChF,MAAM,EAAE,UAAU,WAAW,aAAa,UAAU;CACpD,MAAM,YAAY,QAAkC,eAAe,wBAAwB,SAAS,eAAe,oBAAoB,aAAc,QAAQ,WAAW,GAAG,KAAK;;CAGhL,MAAM,OAAO,OAAO,OAAkB,OAAe,YAA8E;EACjI,MAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO,OAAO,OAAO;EAC1D,MAAM,MAAM,UAAU,OAAO,KAAK;EAClC,IAAI,KAAK,OAAO,MAAM,IACpB,OAAO;EACT,OAAO,MAAM;EACb,MAAM,IAAI,kBAAkB,eAAe,MAAM,GAAG,GAAG,KAAK,IAAI;CAClE;CACA,MAAM,wBAAQ,IAAI,IAAgC;CAElD,MAAM,cAAc,QAAsB;EACxC,MAAM,IAAI,GAAG,CAAC,EAAE,KAAK;EACrB,MAAM,OAAO,GAAG;CAClB;;CAGA,MAAM,WAAW,UAA2B;EAC1C,MAAM,MAAM,MAAM;EAClB,WAAW,GAAG;EACd,UAAU,OAAO,OAAO,SAAS,MAAM,GAAG,CAAC;EAC3C,MAAM,IAAI,KAAK,kBAAkB;GAC/B,UAAU,YAAY;IACpB,MAAM,QAAQ,SAAS,MAAM,GAAG;IAChC,OAAO,QAAQ,KAAK,OAAO,OAAO,OAAO,IAAI,QAAQ,OAAO,IAAI,sBAAsB,CAAC;GACzF;GACA,SAAQ,UAAS,UAAU,OAAO,KAAK,KAAK;GAC5C;GACA,GAAI,QAAQ,WAAW,EAAE,WAAW,WAAuB,QAAQ,SAAU,KAAK,MAAM,EAAE,IAAI,CAAC;GAC/F,GAAI,QAAQ,cAAc,EAAE,cAAc,WAAsB,QAAQ,YAAa,KAAK,MAAM,EAAE,IAAI,CAAC;GACvG,GAAI,QAAQ,iBAAiB,EAAE,sBAAsB,QAAQ,eAAgB,GAAG,EAAE,IAAI,CAAC;GACvF,GAAI,QAAQ,eAAe,KAAA,IAAY,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;GAC7E,GAAI,QAAQ,eAAe,KAAA,IAAY,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;EAC/E,CAAC,CAAC;CACJ;;CAGA,MAAM,aAAmB;EACvB,MAAM,UAAU,SAAS,KAAK,CAAC,CAAC,QAAO,UAAS,CAAC,KAAK,KAAK,CAAC;EAC5D,MAAM,QAAQ,IAAI,IAAI,QAAQ,KAAI,UAAS,MAAM,EAAE,CAAC;EACpD,MAAM,QAAQ,IAAI,IAAI,UAAU,MAAM,CAAC,CAAC,KAAI,SAAQ,CAAC,KAAK,KAAK,KAAK,GAAG,CAAC,CAAC;EACzE,KAAK,MAAM,OAAO,MAAM,KAAK,GAC3B,IAAI,CAAC,MAAM,IAAI,GAAG,GAAG;GACnB,WAAW,GAAG;GACd,UAAU,OAAO,GAAG;EACtB;EAEF,KAAK,MAAM,SAAS,SAClB,IAAI,MAAM,IAAI,MAAM,EAAE,MAAM,MAAM,KAChC,QAAQ,KAAK;OAEb,UAAU,OAAO,MAAM,IAAI;GAAE,MAAM,MAAM;GAAM,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;EAAG,CAAC;CAE7H;CAEA,KAAK;CACL,OAAO;EACL,YAAY,SAAS,KAAK;EAC1B,KAAK,OAAO,UAAU;GACpB,MAAM,QAAQ,MAAM,SAAS,IAAI,KAAK;GAEtC,IAAI,CAAC,KAAK,KAAK,GACb,QAAQ,KAAK;GACf,KAAK;GACL,OAAO;EACT;EACA,SAAS,OAAO;GACd,MAAM,UAAU,SAAS,OAAO,EAAE;GAClC,KAAK;GACL,OAAO;EACT;EACA,SAAS,IAAI,SAAS;GACpB,MAAM,QAAQ,SAAS,OAAO,IAAI,IAAI;GACtC,KAAK;GACL,OAAO;EACT;EACA,MAAM,OAAO,WAAW;GACtB,IAAI,OAAO,WAAW,YAAY,EAAE,QAAQ,SAAS;IACnD,IAAI,CAAC,QAAQ,cACX,MAAM,IAAI,MAAM,gDAAgD;IAClE,OAAO,QAAQ,aAAa,MAAM;GACpC;GACA,MAAM,KAAK,OAAO,WAAW,WAAW,SAAS,OAAO;GACxD,MAAM,SAAS,MAAM,SAAS,KAAK,EAAE;GACrC,IAAI,OAAO,IAAI;IACb,UAAU,OAAO,IAAI;KAAE,YAAY,OAAO,QAAQ;KAAM,GAAI,OAAO,gBAAgB,EAAE,eAAe,OAAO,cAAc,IAAI,CAAC;IAAG,CAAC;IAClI,MAAM,IAAI,EAAE,CAAC,EAAE,MAAM;GACvB;GACA,OAAO;EACT;EACA,QAAO,OAAM,MAAM,IAAI,EAAE,CAAC,EAAE,MAAM;EAClC,gBAAgB;GACd,KAAK,MAAM,QAAQ,MAAM,OAAO,GAC9B,KAAK,MAAM;EACf;EACA,YAAY;GACV,KAAK,MAAM,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC,GAAG;IACnC,WAAW,GAAG;IACd,UAAU,OAAO,GAAG;GACtB;EACF;CACF;AACF;;;;;;;;;;;;;;AC/LA,SAAgB,kBAAkB,KAA+B;CAC/D,IAAI,eAAe,eACjB,OAAO;CACT,IAAI,eAAe,mBACjB,OAAO;CACT,OAAO,eAAe,0BAA0B,aAAa;AAC/D;;;;;;AAOA,SAAgB,mBAAmB,QAAkE;CACnG,OAAO,iBAAiB,aAAa,MAAM;AAC7C;;;;;;;AAqBA,SAAgB,kBAAkB,SAAoD;CACpF,MAAM,EAAE,SAAS,YAAY,SAAS,UAAU,GAAG,SAAS;CAC5D,OAAO,gBAAgB;EACrB,GAAG;EACH,UAAU,YAAY,qBAAqB,EAAE,QAAQ,CAAC;EACtD,UAAU;EACV,cAAc;EACd,SAAS,aAAa,OAAO,OAAO,YAAY,gBAAgB;GAC9D,KAAK,MAAM;GACX;GACA,cAAc,MAAM;GACpB,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC,QAAQ,QAAQ;GAChB,SAAS,QAAQ;EACnB,CAAC;CACH,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"ws-CrD-4Mx6.js","names":[],"sources":["../src/ipc/hosts/probe.ts","../src/ipc/hosts/saved-hosts.ts","../src/ipc/hosts/ws.ts"],"sourcesContent":["/**\n * The host registry's connection (`src/chat/hosts`): a WebSocket probe that\n * says who answers at a URL with a token, and the registry wired to it.\n */\nimport type { HostErrorCode, HostProbeResult, HostRegistry } from '../../chat/hosts'\nimport { createHostRegistry, displayHostUrl } from '../../chat/hosts'\nimport { hostRefOf } from '../protocol'\nimport { IpcVersionMismatchError } from '../transports/client-core'\nimport { hostCaBundle } from '../transports/tls-trust'\nimport { connectWsClient, HostAuthError } from '../transports/ws-client'\n\n/** Bound on one probe: the upgrade, the handshake and `daemon.info`. */\nexport const HOST_PROBE_TIMEOUT_MS = 10_000\n\nconst TLS_FAILURE = /certificate|self[- ]signed|\\btls\\b|\\bssl\\b|UNABLE_TO_VERIFY|CERT_|ERR_TLS/i\n\nfunction classify(err: unknown): { error: HostErrorCode, message: string } {\n if (err instanceof HostAuthError) {\n return err.status === 401\n ? { error: 'auth', message: 'The host refused the token (401). Create a new one on the server: zidane server token create --name <device>.' }\n : { error: 'auth', message: err.message }\n }\n if (err instanceof IpcVersionMismatchError)\n return { error: 'protocol', message: err.message }\n const message = err instanceof Error ? err.message : String(err)\n if (TLS_FAILURE.test(message))\n return { error: 'tls', message: `TLS failed: ${message}` }\n return { error: 'unreachable', message }\n}\n\n/**\n * A failed WebSocket says nothing about why. For `wss://`, ask the same\n * address over HTTPS: a certificate or handshake failure there is a TLS\n * problem, anything else stays \"unreachable\".\n */\nasync function tlsFailure(url: string, signal: AbortSignal): Promise<string | null> {\n if (!url.startsWith('wss:'))\n return null\n try {\n // No token, and no redirect followed: only the TLS handshake with this origin matters.\n // The same CAs as the socket (Bun takes them per request; Node uses its process default).\n const ca = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' ? hostCaBundle() : null\n const response = await fetch(`https:${url.slice('wss:'.length)}`, { redirect: 'manual', signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]), ...(ca ? { tls: { ca } } : {}) } as RequestInit)\n void response.body?.cancel().catch(() => {})\n return null\n }\n catch (err) {\n const e = err as { code?: unknown, message?: unknown }\n const detail = `${typeof e.code === 'string' ? e.code : ''} ${typeof e.message === 'string' ? e.message : ''}`\n return TLS_FAILURE.test(detail) ? `TLS failed: ${String(e.message ?? e.code)}` : null\n }\n}\n\n/**\n * Connect, read the host's identity and `daemon.info`, disconnect. Never throws.\n * @param target - URL, token and an optional timeout in milliseconds. The timeout defaults to {@link HOST_PROBE_TIMEOUT_MS}.\n * @returns Who answered and how fast, or a classified failure.\n */\nexport async function probeWsHost(target: { url: string, token: string, timeoutMs?: number }): Promise<HostProbeResult> {\n const started = performance.now()\n const signal = AbortSignal.timeout(target.timeoutMs ?? HOST_PROBE_TIMEOUT_MS)\n let client: Awaited<ReturnType<typeof connectWsClient>> | undefined\n try {\n client = await connectWsClient({ url: target.url, token: target.token, heartbeatMs: 0, signal })\n const hostRef = hostRefOf(client.hello)\n if (!hostRef)\n return { ok: false, error: 'protocol', message: `${displayHostUrl(target.url)} runs a zidane too old to say which host it is. Upgrade the server.` }\n const info = await client.request('daemon.info', {})\n const serverVersion = info.codeVersion ?? info.appVersion\n return { ok: true, hostRef, ...(serverVersion ? { serverVersion } : {}), latencyMs: Math.round(performance.now() - started) }\n }\n catch (err) {\n if (signal.aborted && !(err instanceof HostAuthError))\n return { ok: false, error: 'unreachable', message: `No answer from ${displayHostUrl(target.url)} within ${target.timeoutMs ?? HOST_PROBE_TIMEOUT_MS} ms.` }\n const classified = classify(err)\n if (classified.error === 'unreachable') {\n const tls = await tlsFailure(target.url, signal).catch(() => null)\n if (tls)\n return { ok: false, error: 'tls', message: tls }\n }\n return { ok: false, ...classified }\n }\n finally {\n client?.close()\n }\n}\n\n/**\n * The registry in `dataDir`, connecting over WebSocket.\n * @param options - Where the registry keeps its data.\n * @returns The host registry.\n */\nexport function createWsHostRegistry(options: { dataDir: string }): HostRegistry {\n return createHostRegistry({ dataDir: options.dataDir, probe: probeWsHost })\n}\n","/**\n * A client's saved servers, kept in its host directory: one directory entry\n * and one app-lifetime connection (`connection-loop`) per server in the\n * registry, in step with it. Shared by the TUI and the GUI main process,\n * which each supply their directory (`SavedHostsDirectory`) and how to dial.\n *\n * Saving a server that is already saved (same host id: a new token, a new\n * URL) reconfigures its entry IN PLACE and reconnects: the entry is never\n * removed, so the sessions routed to it stay routed. Only a server leaving\n * the registry (removed here or by another window) or `stop()` removes an\n * entry, and the directory decides what that means for an open session.\n *\n * Every connection is checked against the saved entry's id (the server's\n * `hostId` when it was added): another host answering at the URL is closed\n * at once, reported as `identity` and not retried, so nothing it says is\n * recorded under the saved id. Dialers may check first (`expectHostId`).\n *\n * No WebSocket import: the WebSocket wiring is `./ws.ts`.\n */\nimport type { HostEntry, HostProbeResult, HostRegistry, HostTestResult } from '../../chat/hosts'\nimport type { HostSignal } from '../protocol'\nimport type { IpcClient } from '../transports/client-core'\nimport type { HostConnectionLoop, HostConnectionPatch, HostFailureKind } from './connection-loop'\nimport { displayHostUrl, HostIdentityError } from '../../chat/hosts'\nimport { hostRefOf } from '../protocol'\nimport { keepHostConnected } from './connection-loop'\n\n/** The client's host directory, as the saved servers see it. Keys are host ids. */\nexport interface SavedHostsDirectory {\n /** The saved servers it holds now, with their URL (a `--host` server excluded). */\n saved: () => { key: string, url: string }[]\n /** Add the entry, or reconfigure the one under its key in place (status `connecting`). */\n upsert: (entry: HostEntry, token: string | undefined) => void\n /** Applies a connection patch, a new name or a server version to the entry. */\n update: (key: string, patch: HostConnectionPatch & { name?: string, serverVersion?: string }) => void\n /** The server left the registry, or the saved servers stopped. */\n remove: (key: string) => void\n}\n\n/** No token is saved for this server (removed by hand, or another window removed it). */\nexport class MissingHostTokenError extends Error {\n constructor() {\n super('No token saved for this server: add it again.')\n this.name = 'MissingHostTokenError'\n }\n}\n\n/** Options for {@link startSavedHosts}: the registry, the host directory, the connect function and callbacks. */\nexport interface SavedHostsOptions {\n /** The registry of saved servers. */\n registry: HostRegistry\n /** The client's host directory the entries are kept in. */\n directory: SavedHostsDirectory\n /** One connect attempt to a saved server, with its token read fresh. */\n connect: (entry: HostEntry, token: string, options: { signal: AbortSignal, onClose: () => void }) => Promise<IpcClient>\n /** `auth`: refused for good. A missing token is always `auth`, another host answering always `identity`. */\n classify?: (err: unknown) => HostFailureKind\n /** The add form's test of an unsaved URL + token (`probeUnsavedHost`). */\n probeUnsaved?: (target: { url: string, token: string }) => Promise<HostProbeResult>\n /** Saved entries the directory already reaches another way (the TUI's `--host` server). */\n skip?: (entry: HostEntry) => boolean\n /** Called with every signal of a connected saved server. */\n onSignal?: (key: string, signal: HostSignal) => void\n /** Called when a saved server connects, also after a reconnect. */\n onConnected?: (key: string, client: IpcClient) => void\n /** Called when a saved server's connection drops. */\n onDisconnected?: (key: string) => void\n /**\n * Lower backoff bound in milliseconds.\n * @default 1000\n */\n minDelayMs?: number\n /**\n * Upper backoff bound in milliseconds.\n * @default 30000\n */\n maxDelayMs?: number\n}\n\n/** Handle returned by {@link startSavedHosts} to list, add, remove, rename, test and retry saved servers. */\nexport interface SavedHostsController {\n /** The saved hosts, read fresh (other windows share the file). */\n list: () => HostEntry[]\n /** Verify, save, connect (in place when already saved). Throws a `HostRegistryError`. */\n add: (input: { url: string, token: string, name?: string }) => Promise<HostEntry>\n /** Removes a server from the registry and its entry from the directory. Returns whether it existed. */\n remove: (id: string) => boolean\n /** Renames a saved server and returns the updated entry. */\n rename: (id: string, name: string) => HostEntry\n /** A saved server (by id), or an unsaved URL + token (the add form's test). */\n test: (target: string | { id: string } | { url: string, token: string }) => Promise<HostTestResult>\n /** Reconnect now (after an outage or a token fix). */\n retry: (id: string) => void\n /** Reconnect every saved server that is not connected now, its backoff reset (the computer woke up). */\n retryAll: () => void\n /** Disconnects every saved server and removes their directory entries. The registry is not changed. */\n stop: () => void\n}\n\n/**\n * Starts the connections of a client's saved servers and keeps the directory in step with the registry.\n * Each saved server gets a directory entry and a connection loop. A connection whose host id differs from the saved one is closed and reported as `identity`.\n * @param options - Registry, directory and how to dial.\n * @returns A controller over the saved servers.\n */\nexport function startSavedHosts(options: SavedHostsOptions): SavedHostsController {\n const { registry, directory, skip = () => false } = options\n const classify = (err: unknown): HostFailureKind => err instanceof MissingHostTokenError ? 'auth' : err instanceof HostIdentityError ? 'identity' : (options.classify?.(err) ?? 'retry')\n\n /** The saved server, or nobody: a connection from another host is closed before anyone sees it. */\n const dial = async (entry: HostEntry, token: string, attempt: { signal: AbortSignal, onClose: () => void }): Promise<IpcClient> => {\n const client = await options.connect(entry, token, attempt)\n const ref = hostRefOf(client.hello)\n if (ref?.id === entry.id)\n return client\n client.close()\n throw new HostIdentityError(displayHostUrl(entry.url), ref?.name)\n }\n const loops = new Map<string, HostConnectionLoop>()\n\n const disconnect = (key: string): void => {\n loops.get(key)?.stop()\n loops.delete(key)\n }\n\n /** (Re)connect `entry` with the token saved now, its directory entry kept or created in place. */\n const connect = (entry: HostEntry): void => {\n const key = entry.id\n disconnect(key)\n directory.upsert(entry, registry.token(key))\n loops.set(key, keepHostConnected({\n connect: (attempt) => {\n const token = registry.token(key)\n return token ? dial(entry, token, attempt) : Promise.reject(new MissingHostTokenError())\n },\n update: patch => directory.update(key, patch),\n classify,\n ...(options.onSignal ? { onSignal: (signal: HostSignal) => options.onSignal!(key, signal) } : {}),\n ...(options.onConnected ? { onConnected: (client: IpcClient) => options.onConnected!(key, client) } : {}),\n ...(options.onDisconnected ? { onDisconnected: () => options.onDisconnected!(key) } : {}),\n ...(options.minDelayMs !== undefined ? { minDelayMs: options.minDelayMs } : {}),\n ...(options.maxDelayMs !== undefined ? { maxDelayMs: options.maxDelayMs } : {}),\n }))\n }\n\n /** Bring the directory in line with the saved hosts. */\n const sync = (): void => {\n const entries = registry.list().filter(entry => !skip(entry))\n const saved = new Set(entries.map(entry => entry.id))\n const known = new Map(directory.saved().map(host => [host.key, host.url]))\n for (const key of known.keys()) {\n if (!saved.has(key)) {\n disconnect(key)\n directory.remove(key)\n }\n }\n for (const entry of entries) {\n if (known.get(entry.id) !== entry.url)\n connect(entry)\n else\n directory.update(entry.id, { name: entry.name, ...(entry.serverVersion ? { serverVersion: entry.serverVersion } : {}) })\n }\n }\n\n sync()\n return {\n list: () => registry.list(),\n add: async (input) => {\n const entry = await registry.add(input)\n // Saved again (a new token or URL): reconnect with them, the entry kept.\n if (!skip(entry))\n connect(entry)\n sync()\n return entry\n },\n remove: (id) => {\n const removed = registry.remove(id)\n sync()\n return removed\n },\n rename: (id, name) => {\n const entry = registry.rename(id, name)\n sync()\n return entry\n },\n test: async (target) => {\n if (typeof target === 'object' && !('id' in target)) {\n if (!options.probeUnsaved)\n throw new Error('Testing an unsaved host is not available here.')\n return options.probeUnsaved(target)\n }\n const id = typeof target === 'string' ? target : target.id\n const result = await registry.test(id)\n if (result.ok) {\n directory.update(id, { serverName: result.hostRef.name, ...(result.serverVersion ? { serverVersion: result.serverVersion } : {}) })\n loops.get(id)?.retry()\n }\n return result\n },\n retry: id => loops.get(id)?.retry(),\n retryAll: () => {\n for (const loop of loops.values())\n loop.retry()\n },\n stop: () => {\n for (const key of [...loops.keys()]) {\n disconnect(key)\n directory.remove(key)\n }\n },\n }\n}\n","/**\n * Saved servers over WebSocket: the registry in `dataDir`, the add form's\n * unsaved test, the refusal classification and (by default) the dial, wired\n * for `startSavedHosts`. Loaded behind each client's remote-hosts gate.\n */\nimport type { HostProbeResult } from '../../chat/hosts'\nimport type { HostFailureKind } from './connection-loop'\nimport type { SavedHostsController, SavedHostsOptions } from './saved-hosts'\nimport { HostIdentityError, probeUnsavedHost } from '../../chat/hosts'\nimport { IpcVersionMismatchError } from '../transports/client-core'\nimport { connectWsClient, HostAuthError } from '../transports/ws-client'\nimport { createWsHostRegistry, probeWsHost } from './probe'\nimport { startSavedHosts } from './saved-hosts'\n\n/**\n * A refused token stays refused until the user changes something: no retry\n * loop. Another protocol version stays until one side is upgraded: a slow re-check.\n * @param err - The error a connect attempt failed with.\n * @returns `auth` for a refused token, `identity` for another host answering, `protocol` for a version mismatch, `retry` otherwise.\n */\nexport function classifyWsFailure(err: unknown): HostFailureKind {\n if (err instanceof HostAuthError)\n return 'auth'\n if (err instanceof HostIdentityError)\n return 'identity'\n return err instanceof IpcVersionMismatchError ? 'protocol' : 'retry'\n}\n\n/**\n * The add form's \"test\": the registry's own checks and probe over WebSocket, saving nothing.\n * @param target - URL and token to try.\n * @returns The probe result.\n */\nexport function probeUnsavedWsHost(target: { url: string, token: string }): Promise<HostProbeResult> {\n return probeUnsavedHost(probeWsHost, target)\n}\n\n/**\n * Options for {@link startWsSavedHosts}: {@link SavedHostsOptions} with the registry, dialer and classifier filled in.\n * `dataDir` holds the saved-hosts registry file.\n */\nexport type WsSavedHostsOptions = Omit<SavedHostsOptions, 'registry' | 'connect' | 'classify' | 'probeUnsaved'> & {\n dataDir: string\n /** Client version in the server's diagnostics (default dial only). */\n appVersion?: string\n /** Dial another way (the TUI goes through its directory entry's transport). Default: `connectWsClient`. */\n connect?: SavedHostsOptions['connect']\n registry?: SavedHostsOptions['registry']\n}\n\n/**\n * Keeps the saved servers of a client connected over WebSocket.\n * Uses the registry in `dataDir` unless `registry` is given, and dials with {@link connectWsClient} unless `connect` is given.\n * @param options - Data directory, host directory and callbacks.\n * @returns A controller to list, add, remove, test and retry hosts, and to stop them all.\n */\nexport function startWsSavedHosts(options: WsSavedHostsOptions): SavedHostsController {\n const { dataDir, appVersion, connect, registry, ...rest } = options\n return startSavedHosts({\n ...rest,\n registry: registry ?? createWsHostRegistry({ dataDir }),\n classify: classifyWsFailure,\n probeUnsaved: probeUnsavedWsHost,\n connect: connect ?? ((entry, token, attempt) => connectWsClient({\n url: entry.url,\n token,\n expectHostId: entry.id,\n ...(appVersion ? { appVersion } : {}),\n signal: attempt.signal,\n onClose: attempt.onClose,\n })),\n })\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.js";
|
|
2
2
|
import { a as errorResponse, i as createJsonRpcEndpoint, o as successResponse } from "./json-rpc-G0rBfEar.js";
|
|
3
3
|
import { i as METHOD_NOT_FOUND, t as HostIdentityError } from "./connection-BaHd3T-D.js";
|
|
4
|
-
import { a as createSignalListeners, o as openIpcClient } from "./settings-keys-
|
|
4
|
+
import { a as createSignalListeners, o as openIpcClient } from "./settings-keys-CnrJgT1T.js";
|
|
5
5
|
import { u as hostRefOf } from "./protocol-Do_i6Ras.js";
|
|
6
6
|
import { n as displayHostUrl } from "./url-D4vG7lzO.js";
|
|
7
7
|
import tls from "node:tls";
|
|
@@ -323,4 +323,4 @@ async function connectWsClient(options) {
|
|
|
323
323
|
//#endregion
|
|
324
324
|
export { hostCaBundle as a, ws_client_exports as i, HostBusyError as n, trustSystemCertificates as o, connectWsClient as r, HostAuthError as t };
|
|
325
325
|
|
|
326
|
-
//# sourceMappingURL=ws-client-
|
|
326
|
+
//# sourceMappingURL=ws-client-Dond8vT3.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-client-DPC9ZJHr.js","names":["BunWebSocket"],"sources":["../src/ipc/transports/tls-trust.ts","../src/ipc/transports/ws-auth.ts","../src/ipc/transports/ws-client.ts"],"sourcesContent":["/**\n * Which certificate authorities a client trusts for a remote zidane-server:\n * the runtime's own roots plus the operating system's store (the macOS\n * keychain, the Linux CA bundle). A server behind a private CA, an mkcert\n * certificate or a self-signed one the user trusted in the OS then works\n * like a public one. `NODE_EXTRA_CA_CERTS` adds certificates at launch, in\n * both runtimes.\n *\n * Bun's WebSocket and fetch ignore `tls.setDefaultCACertificates`, so Bun\n * clients (the TUI, `zidane machine share`) pass the bundle with each\n * connection (`hostCaBundle`). Node honors it, so the desktop app's main\n * process trusts the store once, when it turns remote hosts on\n * (`trustSystemCertificates`).\n */\nimport tls from 'node:tls'\n\ntype CaKind = 'default' | 'system' | 'extra'\nconst api = tls as unknown as {\n getCACertificates?: (kind: CaKind) => string[]\n setDefaultCACertificates?: (certs: readonly string[]) => void\n}\n\nfunction certificates(kind: CaKind): string[] {\n try {\n return api.getCACertificates?.(kind) ?? []\n }\n catch {\n return []\n }\n}\n\nlet bundle: string[] | null | undefined\n\n/**\n * The runtime's roots, the OS store's and `NODE_EXTRA_CA_CERTS`'s, read once.\n * `null` when the OS store adds nothing (or can't be read): the runtime's\n * default then applies as it is.\n */\nexport function hostCaBundle(): string[] | null {\n if (bundle === undefined) {\n const system = certificates('system')\n bundle = system.length === 0 ? null : [...new Set([...certificates('default'), ...system, ...certificates('extra')])]\n }\n return bundle\n}\n\n/**\n * Node: every TLS client of this process trusts {@link hostCaBundle} from now\n * on (the desktop app's main process). Whether it applied.\n */\nexport function trustSystemCertificates(): boolean {\n const ca = hostCaBundle()\n if (!ca || !api.setDefaultCACertificates || typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined')\n return false\n api.setDefaultCACertificates(ca)\n return true\n}\n","/**\n * How a WebSocket client's token reaches the server, shared by both ends.\n *\n * Preferred: `Authorization: Bearer <token>` (Bun's WebSocket can set it).\n * Fallback for clients that can't set handshake headers (Node's and the\n * browser's WebSocket): a `zidane.bearer.<token>` entry in\n * `Sec-WebSocket-Protocol`, offered next to `zidane`, the subprotocol the\n * server selects. A token sent this way must be a valid subprotocol token\n * (base64url is).\n */\n\n/** The subprotocol clients offer and the server selects. */\nexport const WS_SUBPROTOCOL = 'zidane'\n/** Prefix of the subprotocol entry carrying the bearer token. */\nexport const WS_BEARER_PREFIX = 'zidane.bearer.'\n\nexport type WsTokenCarrier = 'header' | 'subprotocol'\n\n/**\n * What an accepted connection is: a `client` (a window: the whole method\n * table, signals) or a `machine` lending its execution (`docs/MACHINES.md`:\n * only `ipc.hello`, `$/ping`, `machine.register`, never a peer). Default `client`.\n */\nexport type WsConnectionScope = 'client' | 'machine'\n\n/**\n * A server's answer to an upgrade's token. Accepted: `principal` names what\n * authenticated (a token id), so connections can be dropped by it later.\n * Refused: the HTTP status, sent before any JSON-RPC (401 no valid\n * credential, 403 a credential not allowed here).\n */\nexport type WsAuthVerdict\n = | { ok: true, principal?: string, scope?: WsConnectionScope }\n | { ok: false, status: 401 | 403, reason?: WsRefusalReason }\n\n/**\n * Why a server refused an upgrade with a 403, as a machine-readable code in\n * {@link WS_REFUSAL_HEADER}. Clients show a fixed text per code (never the\n * response body, which a proxy may fill with anything, the request's own\n * headers included) and ignore codes they don't know.\n */\nexport const WS_REFUSAL_HEADER = 'X-Zidane-Refusal'\nconst WS_REFUSAL_REASONS = ['origin', 'insecure-transport', 'untrusted-proxy', 'scope'] as const\nexport type WsRefusalReason = typeof WS_REFUSAL_REASONS[number]\n\nexport function isWsRefusalReason(value: unknown): value is WsRefusalReason {\n return typeof value === 'string' && (WS_REFUSAL_REASONS as readonly string[]).includes(value)\n}\n\n/** Subprotocols offered in `Sec-WebSocket-Protocol`. */\nexport function offeredSubprotocols(request: Request): string[] {\n return (request.headers.get('sec-websocket-protocol') ?? '').split(',').map(p => p.trim()).filter(Boolean)\n}\n\n/** The client's token: `Authorization: Bearer`, else the subprotocol entry. */\nexport function requestToken(request: Request): string | undefined {\n const auth = request.headers.get('authorization')\n const bearer = auth && /^Bearer\\s+(\\S+)$/i.exec(auth.trim())\n if (bearer)\n return bearer[1]\n const entry = offeredSubprotocols(request).find(p => p.startsWith(WS_BEARER_PREFIX))\n return entry ? entry.slice(WS_BEARER_PREFIX.length) : undefined\n}\n","/**\n * `IpcClient` over a WebSocket (a remote `zidane-server`, `./ws-server`):\n * one JSON-RPC message per text frame, the same handshake, deadlines and\n * signal delivery as the unix-socket client (`./client-core`).\n *\n * Uses the runtime's global `WebSocket` (Bun; Node >= 22), so it is safe in\n * the Node-imported `zidane/ipc` barrel. Unlike a local daemon client it\n * never asks the host to drain on a version mismatch (a remote server's\n * lifetime is not a client's to end) and opts out of the dev build fence.\n *\n * Opening is bounded (`openTimeoutMs`) and abortable (`signal`, up to the\n * end of the handshake). A refused upgrade is opaque to a WebSocket (no\n * status, just \"expected 101\", close 1002), so on failure the client asks\n * the same URL over plain HTTP, with the same token, why: a 401 or 403\n * becomes a {@link HostAuthError}, a 429 a {@link HostBusyError}, each with a\n * fixed message (the response body\n * is never read: a proxy may echo the request's headers, the token\n * included, and the message reaches a GUI renderer). That request never\n * follows a redirect: a 3xx is reported as a misconfigured URL, and the\n * token goes nowhere but the URL's own origin.\n *\n * Messages name the URL by scheme, host and path only (`displayHostUrl`).\n *\n * Certificates: the runtime's roots and the OS store (`./tls-trust`). Under\n * Bun the bundle goes with each socket and refusal probe; under Node the\n * process default applies (the desktop app sets it).\n */\nimport type { IpcClient } from './client-core'\nimport type { WsRefusalReason, WsTokenCarrier } from './ws-auth'\nimport { createJsonRpcEndpoint, errorResponse, successResponse } from '../../acp/json-rpc'\nimport { HostIdentityError, METHOD_NOT_FOUND } from '../../chat/hosts/connection'\nimport { displayHostUrl } from '../../chat/hosts/url'\nimport { hostRefOf } from '../protocol'\nimport { createSignalListeners, openIpcClient } from './client-core'\nimport { hostCaBundle } from './tls-trust'\nimport { isWsRefusalReason, WS_BEARER_PREFIX, WS_REFUSAL_HEADER, WS_SUBPROTOCOL } from './ws-auth'\n\n/** Options for {@link connectWsClient}. */\nexport interface WsClientOptions {\n /** `ws://` or `wss://` URL of the server. */\n url: string\n /** Bearer token for the server, see `./ws-auth`. */\n token?: string\n /**\n * How the token travels. Default: the `Authorization` header where the\n * runtime's WebSocket can set one (Bun), else the subprotocol fallback.\n */\n tokenCarrier?: WsTokenCarrier\n /** Client zidane version, surfaced in the host's diagnostics. */\n appVersion?: string\n /**\n * Certificate authorities (PEM) a `wss://` server's certificate may chain\n * to. Default: the runtime's roots and the OS store (`hostCaBundle`). Bun\n * only: under Node the process default applies.\n */\n ca?: readonly string[]\n /** The connection dropped (server exit, network error, a missed deadline or heartbeat). Not called for `close()`. */\n onClose?: () => void\n /** `$/ping` interval (`./client-core`). Default {@link WS_HEARTBEAT_MS}; `0` disables. */\n heartbeatMs?: number\n /** Bound on the upgrade (not the handshake, which has its own). Default {@link WS_OPEN_TIMEOUT_MS}. */\n openTimeoutMs?: number\n /** Abandon the connect (upgrade and handshake): it rejects with the signal's reason. */\n signal?: AbortSignal\n /**\n * The host id a saved host answered with when it was added: a hello with\n * any other id closes the connection and rejects with a\n * {@link HostIdentityError} (nothing is read from that host).\n */\n expectHostId?: string\n /**\n * Answer the host's requests and notifications on this connection: a\n * machine serving its host (`docs/MACHINES.md`). A throw becomes a\n * JSON-RPC error (its `code` when it carries one). Default: a client\n * refuses requests and reads only `ipc.signal`.\n */\n serve?: {\n request: (method: string, params: unknown) => Promise<unknown>\n notification?: (method: string, params: unknown) => void\n }\n}\n\n/** Heartbeat interval on both ends of a WebSocket by default. */\nexport const WS_HEARTBEAT_MS = 20_000\n/** How long an upgrade may take by default. */\nconst WS_OPEN_TIMEOUT_MS = 15_000\n\nconst FORBIDDEN: Record<WsRefusalReason, string> = {\n 'origin': 'this client\\'s origin is not allowed by the server (allowedOrigins)',\n 'insecure-transport': 'the server only accepts connections through its HTTPS proxy',\n 'untrusted-proxy': 'the server only accepts connections from its trusted proxies',\n 'scope': 'this token\\'s scope cannot open a client connection',\n}\n\n/**\n * The server refused this client's credential: 401 (no valid token) or 403\n * (not allowed here). `reason` only when the server sent a code clients know.\n */\nexport class HostAuthError extends Error {\n /** The server URL, scheme, host and path only. */\n readonly url: string\n /**\n * @param url - The server URL.\n * @param status - HTTP status of the refusal.\n * @param reason - Refusal code the server sent, when any.\n */\n constructor(url: string, readonly status: 401 | 403, readonly reason?: WsRefusalReason) {\n const shown = displayHostUrl(url)\n super(`WebSocket connection to ${shown} failed: ${status === 401\n ? 'token refused (401): the server does not know this token, or it was revoked. Create a new one on the server (zidane server token create --name <device>) and use it here'\n : `forbidden (403): ${reason ? FORBIDDEN[reason] : 'refused by the server or a proxy in front of it (origin, token scope or proxy rules)'}`}.`)\n this.name = 'HostAuthError'\n this.url = shown\n }\n}\n\n/** The server turned the upgrade down with a 429: too many open connections (per token, per address) or upgrades (per address). Transient. */\nexport class HostBusyError extends Error {\n /** @param url - The server URL. */\n constructor(url: string) {\n super(`WebSocket connection to ${displayHostUrl(url)} failed: too many connections or connection attempts (429). The server caps open connections per token and per address: close other windows that use this token, or wait a few seconds.`)\n this.name = 'HostBusyError'\n }\n}\n\n/** The URL answered with a redirect: not followed (the token would go along). */\nexport class HostRedirectError extends Error {\n constructor(url: string, status: number) {\n super(`WebSocket connection to ${displayHostUrl(url)} failed: the server redirects (${status}); not following it with the token. Check the URL (scheme, host, path) or the proxy in front of the server.`)\n this.name = 'HostRedirectError'\n }\n}\n\n/** Bun's WebSocket and fetch take handshake headers and TLS options; the WHATWG ones (Node, browsers) do not. */\nconst isBun = (): boolean => typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'\n\n/** Under Bun, the CAs to pass along for a `wss:` / `https:` URL; `null`: the runtime's default. */\nfunction bunCa(options: WsClientOptions): readonly string[] | null {\n return isBun() && /^(?:wss|https):/i.test(options.url) ? options.ca ?? hostCaBundle() : null\n}\n\nfunction createSocket(options: WsClientOptions): WebSocket {\n const protocols = [WS_SUBPROTOCOL]\n const carrier = options.tokenCarrier ?? (isBun() ? 'header' : 'subprotocol')\n if (options.token && carrier === 'subprotocol')\n protocols.push(`${WS_BEARER_PREFIX}${options.token}`)\n const headers = options.token && carrier === 'header' ? { Authorization: `Bearer ${options.token}` } : null\n const ca = bunCa(options)\n if (headers || ca) {\n // Bun's constructor extension: `{ headers, protocols, tls }` in place of the protocol list.\n const BunWebSocket = WebSocket as unknown as new (url: string, init: { headers?: Record<string, string>, protocols: string[], tls?: { ca: readonly string[] } }) => WebSocket\n return new BunWebSocket(options.url, { ...(headers ? { headers } : {}), protocols, ...(ca ? { tls: { ca } } : {}) })\n }\n return new WebSocket(options.url, protocols)\n}\n\n/** The upgrade took longer than `openTimeoutMs`: nothing answered, so there is no refusal to ask about. */\nclass WsOpenTimeoutError extends Error {\n constructor(url: string, timeoutMs: number) {\n super(`WebSocket connection to ${displayHostUrl(url)} timed out after ${timeoutMs} ms.`)\n this.name = 'WsOpenTimeoutError'\n }\n}\n\nfunction abortError(signal: AbortSignal): Error {\n return signal.reason instanceof Error ? signal.reason : new DOMException('The connect was aborted.', 'AbortError')\n}\n\n/** Resolves on `open`; rejects on failure, timeout or abort (closing the socket). */\nfunction opened(ws: WebSocket, url: string, timeoutMs: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve, reject) => {\n const done = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const settle = (err?: Error): void => {\n if (done.signal.aborted)\n return\n done.abort()\n clearTimeout(timer)\n if (!err)\n return resolve()\n ws.close()\n reject(err)\n }\n const fail = (detail: string) => () => settle(new Error(`WebSocket connection to ${displayHostUrl(url)} failed${detail}.`))\n ws.addEventListener('open', () => settle(), { signal: done.signal })\n ws.addEventListener('error', fail(''), { signal: done.signal })\n ws.addEventListener('close', event => fail(` (close ${event.code}${event.reason ? `: ${event.reason}` : ''})`)(), { signal: done.signal })\n signal?.addEventListener('abort', () => settle(abortError(signal)), { signal: done.signal })\n timer = setTimeout(() => settle(new WsOpenTimeoutError(url, timeoutMs)), timeoutMs)\n })\n}\n\n/** Why an upgrade failed, asked over plain HTTP: a {@link HostAuthError} for 401 / 403, a {@link HostBusyError} for 429, a {@link HostRedirectError} for 3xx, else `null`. */\nasync function refusal(options: WsClientOptions): Promise<Error | null> {\n try {\n const url = new URL(options.url)\n url.protocol = url.protocol === 'wss:' ? 'https:' : 'http:'\n const timeout = AbortSignal.timeout(5000)\n const ca = bunCa(options)\n const response = await fetch(url, {\n headers: options.token ? { Authorization: `Bearer ${options.token}` } : {},\n redirect: 'manual',\n signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,\n // Bun's fetch extension, as for the socket.\n ...(ca ? { tls: { ca } } : {}),\n } as RequestInit)\n void response.body?.cancel().catch(() => {})\n if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400))\n return new HostRedirectError(options.url, response.status)\n if (response.status === 429)\n return new HostBusyError(options.url)\n if (response.status !== 401 && response.status !== 403)\n return null\n const reason = response.headers.get(WS_REFUSAL_HEADER)\n return new HostAuthError(options.url, response.status, response.status === 403 && isWsRefusalReason(reason) ? reason : undefined)\n }\n catch {\n return null\n }\n}\n\n/** Close a connection whose host is not the one expected, and say so. */\nfunction checkIdentity(client: IpcClient, options: WsClientOptions): IpcClient {\n if (options.expectHostId === undefined)\n return client\n const ref = hostRefOf(client.hello)\n if (ref?.id === options.expectHostId)\n return client\n client.close()\n throw new HostIdentityError(displayHostUrl(options.url), ref?.name)\n}\n\n/**\n * Connects to a remote zidane server over WebSocket and completes the `ipc.hello` handshake.\n * A refused upgrade rejects with {@link HostAuthError} for a 401 or 403, and {@link HostBusyError} for a 429. A hello from another host than `expectHostId` rejects with `HostIdentityError`. A version mismatch rejects with {@link IpcVersionMismatchError} and leaves the server running.\n * @param options - Server URL, token and connection settings.\n * @returns A typed client.\n * @example\n * ```ts\n * import { connectWsClient } from 'zidane/ipc/remote'\n *\n * const client = await connectWsClient({\n * url: 'wss://zidane.example.com/',\n * token: process.env.ZIDANE_HOST_TOKEN,\n * })\n * const sessions = await client.request('sessions.list', {})\n * console.log(sessions.length)\n * client.close()\n * ```\n */\nexport async function connectWsClient(options: WsClientOptions): Promise<IpcClient> {\n const { signal } = options\n signal?.throwIfAborted()\n const ws = createSocket(options)\n ws.binaryType = 'arraybuffer'\n try {\n await opened(ws, options.url, options.openTimeoutMs ?? WS_OPEN_TIMEOUT_MS, signal)\n }\n catch (err) {\n if (signal?.aborted || err instanceof WsOpenTimeoutError)\n throw err\n throw (await refusal(options)) ?? err\n }\n\n const signals = createSignalListeners()\n let closed = false\n\n const rpc = createJsonRpcEndpoint({\n send: (json) => {\n try {\n ws.send(json)\n }\n catch {\n // A closing socket: the close handler fails what is pending.\n }\n },\n // The host never sends requests to clients, only notifications; a machine serves them.\n onRequest: async (message) => {\n const serve = options.serve\n if (!serve)\n return errorResponse(message.id, METHOD_NOT_FOUND, 'Clients do not accept requests.')\n try {\n return successResponse(message.id, (await serve.request(message.method, message.params)) ?? null)\n }\n catch (err) {\n const code = (err as { code?: unknown }).code\n return errorResponse(message.id, typeof code === 'number' ? code : -32000, err instanceof Error ? err.message : String(err))\n }\n },\n onNotification: (message) => {\n if (message.method === 'ipc.signal' || !options.serve?.notification)\n return signals.onNotification(message)\n options.serve.notification(message.method, (message as { params?: unknown }).params)\n },\n })\n\n const decoder = new TextDecoder()\n ws.addEventListener('message', (event: MessageEvent) => {\n if (closed)\n return\n rpc.receive(typeof event.data === 'string' ? event.data : decoder.decode(event.data as ArrayBuffer))\n })\n\n /** The host went away (or the connection was given up): fail what is pending, tell the owner. */\n const lost = (): void => {\n if (closed)\n return\n closed = true\n rpc.close()\n options.onClose?.()\n }\n ws.addEventListener('close', lost)\n ws.addEventListener('error', () => {\n // 'close' follows.\n })\n\n const close = (): void => {\n if (closed)\n return\n closed = true\n rpc.close()\n ws.close(1000)\n }\n\n // Aborted mid-handshake: the close fails the pending hello, and the connect rejects with the signal's reason.\n const onAbort = (): void => close()\n signal?.addEventListener('abort', onAbort, { once: true })\n try {\n return checkIdentity(await openIpcClient(\n {\n rpc,\n close,\n abort: () => {\n lost()\n ws.close(1000)\n },\n where: displayHostUrl(options.url),\n },\n signals.listeners,\n {\n ...(options.appVersion ? { appVersion: options.appVersion } : {}),\n drain: 'never',\n buildFence: false,\n heartbeatMs: options.heartbeatMs ?? WS_HEARTBEAT_MS,\n },\n ), options)\n }\n catch (err) {\n throw signal?.aborted ? abortError(signal) : err\n }\n finally {\n signal?.removeEventListener('abort', onAbort)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ws-client-Dond8vT3.js","names":["BunWebSocket"],"sources":["../src/ipc/transports/tls-trust.ts","../src/ipc/transports/ws-auth.ts","../src/ipc/transports/ws-client.ts"],"sourcesContent":["/**\n * Which certificate authorities a client trusts for a remote zidane-server:\n * the runtime's own roots plus the operating system's store (the macOS\n * keychain, the Linux CA bundle). A server behind a private CA, an mkcert\n * certificate or a self-signed one the user trusted in the OS then works\n * like a public one. `NODE_EXTRA_CA_CERTS` adds certificates at launch, in\n * both runtimes.\n *\n * Bun's WebSocket and fetch ignore `tls.setDefaultCACertificates`, so Bun\n * clients (the TUI, `zidane machine share`) pass the bundle with each\n * connection (`hostCaBundle`). Node honors it, so the desktop app's main\n * process trusts the store once, when it turns remote hosts on\n * (`trustSystemCertificates`).\n */\nimport tls from 'node:tls'\n\ntype CaKind = 'default' | 'system' | 'extra'\nconst api = tls as unknown as {\n getCACertificates?: (kind: CaKind) => string[]\n setDefaultCACertificates?: (certs: readonly string[]) => void\n}\n\nfunction certificates(kind: CaKind): string[] {\n try {\n return api.getCACertificates?.(kind) ?? []\n }\n catch {\n return []\n }\n}\n\nlet bundle: string[] | null | undefined\n\n/**\n * The runtime's roots, the OS store's and `NODE_EXTRA_CA_CERTS`'s, read once.\n * `null` when the OS store adds nothing (or can't be read): the runtime's\n * default then applies as it is.\n */\nexport function hostCaBundle(): string[] | null {\n if (bundle === undefined) {\n const system = certificates('system')\n bundle = system.length === 0 ? null : [...new Set([...certificates('default'), ...system, ...certificates('extra')])]\n }\n return bundle\n}\n\n/**\n * Node: every TLS client of this process trusts {@link hostCaBundle} from now\n * on (the desktop app's main process). Whether it applied.\n */\nexport function trustSystemCertificates(): boolean {\n const ca = hostCaBundle()\n if (!ca || !api.setDefaultCACertificates || typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined')\n return false\n api.setDefaultCACertificates(ca)\n return true\n}\n","/**\n * How a WebSocket client's token reaches the server, shared by both ends.\n *\n * Preferred: `Authorization: Bearer <token>` (Bun's WebSocket can set it).\n * Fallback for clients that can't set handshake headers (Node's and the\n * browser's WebSocket): a `zidane.bearer.<token>` entry in\n * `Sec-WebSocket-Protocol`, offered next to `zidane`, the subprotocol the\n * server selects. A token sent this way must be a valid subprotocol token\n * (base64url is).\n */\n\n/** The subprotocol clients offer and the server selects. */\nexport const WS_SUBPROTOCOL = 'zidane'\n/** Prefix of the subprotocol entry carrying the bearer token. */\nexport const WS_BEARER_PREFIX = 'zidane.bearer.'\n\nexport type WsTokenCarrier = 'header' | 'subprotocol'\n\n/**\n * What an accepted connection is: a `client` (a window: the whole method\n * table, signals) or a `machine` lending its execution (`docs/MACHINES.md`:\n * only `ipc.hello`, `$/ping`, `machine.register`, never a peer). Default `client`.\n */\nexport type WsConnectionScope = 'client' | 'machine'\n\n/**\n * A server's answer to an upgrade's token. Accepted: `principal` names what\n * authenticated (a token id), so connections can be dropped by it later.\n * Refused: the HTTP status, sent before any JSON-RPC (401 no valid\n * credential, 403 a credential not allowed here).\n */\nexport type WsAuthVerdict\n = | { ok: true, principal?: string, scope?: WsConnectionScope }\n | { ok: false, status: 401 | 403, reason?: WsRefusalReason }\n\n/**\n * Why a server refused an upgrade with a 403, as a machine-readable code in\n * {@link WS_REFUSAL_HEADER}. Clients show a fixed text per code (never the\n * response body, which a proxy may fill with anything, the request's own\n * headers included) and ignore codes they don't know.\n */\nexport const WS_REFUSAL_HEADER = 'X-Zidane-Refusal'\nconst WS_REFUSAL_REASONS = ['origin', 'insecure-transport', 'untrusted-proxy', 'scope'] as const\nexport type WsRefusalReason = typeof WS_REFUSAL_REASONS[number]\n\nexport function isWsRefusalReason(value: unknown): value is WsRefusalReason {\n return typeof value === 'string' && (WS_REFUSAL_REASONS as readonly string[]).includes(value)\n}\n\n/** Subprotocols offered in `Sec-WebSocket-Protocol`. */\nexport function offeredSubprotocols(request: Request): string[] {\n return (request.headers.get('sec-websocket-protocol') ?? '').split(',').map(p => p.trim()).filter(Boolean)\n}\n\n/** The client's token: `Authorization: Bearer`, else the subprotocol entry. */\nexport function requestToken(request: Request): string | undefined {\n const auth = request.headers.get('authorization')\n const bearer = auth && /^Bearer\\s+(\\S+)$/i.exec(auth.trim())\n if (bearer)\n return bearer[1]\n const entry = offeredSubprotocols(request).find(p => p.startsWith(WS_BEARER_PREFIX))\n return entry ? entry.slice(WS_BEARER_PREFIX.length) : undefined\n}\n","/**\n * `IpcClient` over a WebSocket (a remote `zidane-server`, `./ws-server`):\n * one JSON-RPC message per text frame, the same handshake, deadlines and\n * signal delivery as the unix-socket client (`./client-core`).\n *\n * Uses the runtime's global `WebSocket` (Bun; Node >= 22), so it is safe in\n * the Node-imported `zidane/ipc` barrel. Unlike a local daemon client it\n * never asks the host to drain on a version mismatch (a remote server's\n * lifetime is not a client's to end) and opts out of the dev build fence.\n *\n * Opening is bounded (`openTimeoutMs`) and abortable (`signal`, up to the\n * end of the handshake). A refused upgrade is opaque to a WebSocket (no\n * status, just \"expected 101\", close 1002), so on failure the client asks\n * the same URL over plain HTTP, with the same token, why: a 401 or 403\n * becomes a {@link HostAuthError}, a 429 a {@link HostBusyError}, each with a\n * fixed message (the response body\n * is never read: a proxy may echo the request's headers, the token\n * included, and the message reaches a GUI renderer). That request never\n * follows a redirect: a 3xx is reported as a misconfigured URL, and the\n * token goes nowhere but the URL's own origin.\n *\n * Messages name the URL by scheme, host and path only (`displayHostUrl`).\n *\n * Certificates: the runtime's roots and the OS store (`./tls-trust`). Under\n * Bun the bundle goes with each socket and refusal probe; under Node the\n * process default applies (the desktop app sets it).\n */\nimport type { IpcClient } from './client-core'\nimport type { WsRefusalReason, WsTokenCarrier } from './ws-auth'\nimport { createJsonRpcEndpoint, errorResponse, successResponse } from '../../acp/json-rpc'\nimport { HostIdentityError, METHOD_NOT_FOUND } from '../../chat/hosts/connection'\nimport { displayHostUrl } from '../../chat/hosts/url'\nimport { hostRefOf } from '../protocol'\nimport { createSignalListeners, openIpcClient } from './client-core'\nimport { hostCaBundle } from './tls-trust'\nimport { isWsRefusalReason, WS_BEARER_PREFIX, WS_REFUSAL_HEADER, WS_SUBPROTOCOL } from './ws-auth'\n\n/** Options for {@link connectWsClient}. */\nexport interface WsClientOptions {\n /** `ws://` or `wss://` URL of the server. */\n url: string\n /** Bearer token for the server, see `./ws-auth`. */\n token?: string\n /**\n * How the token travels. Default: the `Authorization` header where the\n * runtime's WebSocket can set one (Bun), else the subprotocol fallback.\n */\n tokenCarrier?: WsTokenCarrier\n /** Client zidane version, surfaced in the host's diagnostics. */\n appVersion?: string\n /**\n * Certificate authorities (PEM) a `wss://` server's certificate may chain\n * to. Default: the runtime's roots and the OS store (`hostCaBundle`). Bun\n * only: under Node the process default applies.\n */\n ca?: readonly string[]\n /** The connection dropped (server exit, network error, a missed deadline or heartbeat). Not called for `close()`. */\n onClose?: () => void\n /** `$/ping` interval (`./client-core`). Default {@link WS_HEARTBEAT_MS}; `0` disables. */\n heartbeatMs?: number\n /** Bound on the upgrade (not the handshake, which has its own). Default {@link WS_OPEN_TIMEOUT_MS}. */\n openTimeoutMs?: number\n /** Abandon the connect (upgrade and handshake): it rejects with the signal's reason. */\n signal?: AbortSignal\n /**\n * The host id a saved host answered with when it was added: a hello with\n * any other id closes the connection and rejects with a\n * {@link HostIdentityError} (nothing is read from that host).\n */\n expectHostId?: string\n /**\n * Answer the host's requests and notifications on this connection: a\n * machine serving its host (`docs/MACHINES.md`). A throw becomes a\n * JSON-RPC error (its `code` when it carries one). Default: a client\n * refuses requests and reads only `ipc.signal`.\n */\n serve?: {\n request: (method: string, params: unknown) => Promise<unknown>\n notification?: (method: string, params: unknown) => void\n }\n}\n\n/** Heartbeat interval on both ends of a WebSocket by default. */\nexport const WS_HEARTBEAT_MS = 20_000\n/** How long an upgrade may take by default. */\nconst WS_OPEN_TIMEOUT_MS = 15_000\n\nconst FORBIDDEN: Record<WsRefusalReason, string> = {\n 'origin': 'this client\\'s origin is not allowed by the server (allowedOrigins)',\n 'insecure-transport': 'the server only accepts connections through its HTTPS proxy',\n 'untrusted-proxy': 'the server only accepts connections from its trusted proxies',\n 'scope': 'this token\\'s scope cannot open a client connection',\n}\n\n/**\n * The server refused this client's credential: 401 (no valid token) or 403\n * (not allowed here). `reason` only when the server sent a code clients know.\n */\nexport class HostAuthError extends Error {\n /** The server URL, scheme, host and path only. */\n readonly url: string\n /**\n * @param url - The server URL.\n * @param status - HTTP status of the refusal.\n * @param reason - Refusal code the server sent, when any.\n */\n constructor(url: string, readonly status: 401 | 403, readonly reason?: WsRefusalReason) {\n const shown = displayHostUrl(url)\n super(`WebSocket connection to ${shown} failed: ${status === 401\n ? 'token refused (401): the server does not know this token, or it was revoked. Create a new one on the server (zidane server token create --name <device>) and use it here'\n : `forbidden (403): ${reason ? FORBIDDEN[reason] : 'refused by the server or a proxy in front of it (origin, token scope or proxy rules)'}`}.`)\n this.name = 'HostAuthError'\n this.url = shown\n }\n}\n\n/** The server turned the upgrade down with a 429: too many open connections (per token, per address) or upgrades (per address). Transient. */\nexport class HostBusyError extends Error {\n /** @param url - The server URL. */\n constructor(url: string) {\n super(`WebSocket connection to ${displayHostUrl(url)} failed: too many connections or connection attempts (429). The server caps open connections per token and per address: close other windows that use this token, or wait a few seconds.`)\n this.name = 'HostBusyError'\n }\n}\n\n/** The URL answered with a redirect: not followed (the token would go along). */\nexport class HostRedirectError extends Error {\n constructor(url: string, status: number) {\n super(`WebSocket connection to ${displayHostUrl(url)} failed: the server redirects (${status}); not following it with the token. Check the URL (scheme, host, path) or the proxy in front of the server.`)\n this.name = 'HostRedirectError'\n }\n}\n\n/** Bun's WebSocket and fetch take handshake headers and TLS options; the WHATWG ones (Node, browsers) do not. */\nconst isBun = (): boolean => typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'\n\n/** Under Bun, the CAs to pass along for a `wss:` / `https:` URL; `null`: the runtime's default. */\nfunction bunCa(options: WsClientOptions): readonly string[] | null {\n return isBun() && /^(?:wss|https):/i.test(options.url) ? options.ca ?? hostCaBundle() : null\n}\n\nfunction createSocket(options: WsClientOptions): WebSocket {\n const protocols = [WS_SUBPROTOCOL]\n const carrier = options.tokenCarrier ?? (isBun() ? 'header' : 'subprotocol')\n if (options.token && carrier === 'subprotocol')\n protocols.push(`${WS_BEARER_PREFIX}${options.token}`)\n const headers = options.token && carrier === 'header' ? { Authorization: `Bearer ${options.token}` } : null\n const ca = bunCa(options)\n if (headers || ca) {\n // Bun's constructor extension: `{ headers, protocols, tls }` in place of the protocol list.\n const BunWebSocket = WebSocket as unknown as new (url: string, init: { headers?: Record<string, string>, protocols: string[], tls?: { ca: readonly string[] } }) => WebSocket\n return new BunWebSocket(options.url, { ...(headers ? { headers } : {}), protocols, ...(ca ? { tls: { ca } } : {}) })\n }\n return new WebSocket(options.url, protocols)\n}\n\n/** The upgrade took longer than `openTimeoutMs`: nothing answered, so there is no refusal to ask about. */\nclass WsOpenTimeoutError extends Error {\n constructor(url: string, timeoutMs: number) {\n super(`WebSocket connection to ${displayHostUrl(url)} timed out after ${timeoutMs} ms.`)\n this.name = 'WsOpenTimeoutError'\n }\n}\n\nfunction abortError(signal: AbortSignal): Error {\n return signal.reason instanceof Error ? signal.reason : new DOMException('The connect was aborted.', 'AbortError')\n}\n\n/** Resolves on `open`; rejects on failure, timeout or abort (closing the socket). */\nfunction opened(ws: WebSocket, url: string, timeoutMs: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve, reject) => {\n const done = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const settle = (err?: Error): void => {\n if (done.signal.aborted)\n return\n done.abort()\n clearTimeout(timer)\n if (!err)\n return resolve()\n ws.close()\n reject(err)\n }\n const fail = (detail: string) => () => settle(new Error(`WebSocket connection to ${displayHostUrl(url)} failed${detail}.`))\n ws.addEventListener('open', () => settle(), { signal: done.signal })\n ws.addEventListener('error', fail(''), { signal: done.signal })\n ws.addEventListener('close', event => fail(` (close ${event.code}${event.reason ? `: ${event.reason}` : ''})`)(), { signal: done.signal })\n signal?.addEventListener('abort', () => settle(abortError(signal)), { signal: done.signal })\n timer = setTimeout(() => settle(new WsOpenTimeoutError(url, timeoutMs)), timeoutMs)\n })\n}\n\n/** Why an upgrade failed, asked over plain HTTP: a {@link HostAuthError} for 401 / 403, a {@link HostBusyError} for 429, a {@link HostRedirectError} for 3xx, else `null`. */\nasync function refusal(options: WsClientOptions): Promise<Error | null> {\n try {\n const url = new URL(options.url)\n url.protocol = url.protocol === 'wss:' ? 'https:' : 'http:'\n const timeout = AbortSignal.timeout(5000)\n const ca = bunCa(options)\n const response = await fetch(url, {\n headers: options.token ? { Authorization: `Bearer ${options.token}` } : {},\n redirect: 'manual',\n signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,\n // Bun's fetch extension, as for the socket.\n ...(ca ? { tls: { ca } } : {}),\n } as RequestInit)\n void response.body?.cancel().catch(() => {})\n if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400))\n return new HostRedirectError(options.url, response.status)\n if (response.status === 429)\n return new HostBusyError(options.url)\n if (response.status !== 401 && response.status !== 403)\n return null\n const reason = response.headers.get(WS_REFUSAL_HEADER)\n return new HostAuthError(options.url, response.status, response.status === 403 && isWsRefusalReason(reason) ? reason : undefined)\n }\n catch {\n return null\n }\n}\n\n/** Close a connection whose host is not the one expected, and say so. */\nfunction checkIdentity(client: IpcClient, options: WsClientOptions): IpcClient {\n if (options.expectHostId === undefined)\n return client\n const ref = hostRefOf(client.hello)\n if (ref?.id === options.expectHostId)\n return client\n client.close()\n throw new HostIdentityError(displayHostUrl(options.url), ref?.name)\n}\n\n/**\n * Connects to a remote zidane server over WebSocket and completes the `ipc.hello` handshake.\n * A refused upgrade rejects with {@link HostAuthError} for a 401 or 403, and {@link HostBusyError} for a 429. A hello from another host than `expectHostId` rejects with `HostIdentityError`. A version mismatch rejects with {@link IpcVersionMismatchError} and leaves the server running.\n * @param options - Server URL, token and connection settings.\n * @returns A typed client.\n * @example\n * ```ts\n * import { connectWsClient } from 'zidane/ipc/remote'\n *\n * const client = await connectWsClient({\n * url: 'wss://zidane.example.com/',\n * token: process.env.ZIDANE_HOST_TOKEN,\n * })\n * const sessions = await client.request('sessions.list', {})\n * console.log(sessions.length)\n * client.close()\n * ```\n */\nexport async function connectWsClient(options: WsClientOptions): Promise<IpcClient> {\n const { signal } = options\n signal?.throwIfAborted()\n const ws = createSocket(options)\n ws.binaryType = 'arraybuffer'\n try {\n await opened(ws, options.url, options.openTimeoutMs ?? WS_OPEN_TIMEOUT_MS, signal)\n }\n catch (err) {\n if (signal?.aborted || err instanceof WsOpenTimeoutError)\n throw err\n throw (await refusal(options)) ?? err\n }\n\n const signals = createSignalListeners()\n let closed = false\n\n const rpc = createJsonRpcEndpoint({\n send: (json) => {\n try {\n ws.send(json)\n }\n catch {\n // A closing socket: the close handler fails what is pending.\n }\n },\n // The host never sends requests to clients, only notifications; a machine serves them.\n onRequest: async (message) => {\n const serve = options.serve\n if (!serve)\n return errorResponse(message.id, METHOD_NOT_FOUND, 'Clients do not accept requests.')\n try {\n return successResponse(message.id, (await serve.request(message.method, message.params)) ?? null)\n }\n catch (err) {\n const code = (err as { code?: unknown }).code\n return errorResponse(message.id, typeof code === 'number' ? code : -32000, err instanceof Error ? err.message : String(err))\n }\n },\n onNotification: (message) => {\n if (message.method === 'ipc.signal' || !options.serve?.notification)\n return signals.onNotification(message)\n options.serve.notification(message.method, (message as { params?: unknown }).params)\n },\n })\n\n const decoder = new TextDecoder()\n ws.addEventListener('message', (event: MessageEvent) => {\n if (closed)\n return\n rpc.receive(typeof event.data === 'string' ? event.data : decoder.decode(event.data as ArrayBuffer))\n })\n\n /** The host went away (or the connection was given up): fail what is pending, tell the owner. */\n const lost = (): void => {\n if (closed)\n return\n closed = true\n rpc.close()\n options.onClose?.()\n }\n ws.addEventListener('close', lost)\n ws.addEventListener('error', () => {\n // 'close' follows.\n })\n\n const close = (): void => {\n if (closed)\n return\n closed = true\n rpc.close()\n ws.close(1000)\n }\n\n // Aborted mid-handshake: the close fails the pending hello, and the connect rejects with the signal's reason.\n const onAbort = (): void => close()\n signal?.addEventListener('abort', onAbort, { once: true })\n try {\n return checkIdentity(await openIpcClient(\n {\n rpc,\n close,\n abort: () => {\n lost()\n ws.close(1000)\n },\n where: displayHostUrl(options.url),\n },\n signals.listeners,\n {\n ...(options.appVersion ? { appVersion: options.appVersion } : {}),\n drain: 'never',\n buildFence: false,\n heartbeatMs: options.heartbeatMs ?? WS_HEARTBEAT_MS,\n },\n ), options)\n }\n catch (err) {\n throw signal?.aborted ? abortError(signal) : err\n }\n finally {\n signal?.removeEventListener('abort', onAbort)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiBA,MAAM,MAAM;AAKZ,SAAS,aAAa,MAAwB;CAC5C,IAAI;EACF,OAAO,IAAI,oBAAoB,IAAI,KAAK,CAAC;CAC3C,QACM;EACJ,OAAO,CAAC;CACV;AACF;AAEA,IAAI;;;;;;AAOJ,SAAgB,eAAgC;CAC9C,IAAI,WAAW,KAAA,GAAW;EACxB,MAAM,SAAS,aAAa,QAAQ;EACpC,SAAS,OAAO,WAAW,IAAI,OAAO,CAAC,mBAAG,IAAI,IAAI;GAAC,GAAG,aAAa,SAAS;GAAG,GAAG;GAAQ,GAAG,aAAa,OAAO;EAAC,CAAC,CAAC;CACtH;CACA,OAAO;AACT;;;;;AAMA,SAAgB,0BAAmC;CACjD,MAAM,KAAK,aAAa;CACxB,IAAI,CAAC,MAAM,CAAC,IAAI,4BAA4B,OAAQ,WAAiC,QAAQ,aAC3F,OAAO;CACT,IAAI,yBAAyB,EAAE;CAC/B,OAAO;AACT;;;;;;;;;;;;;;AC5CA,MAAa,iBAAiB;;AAE9B,MAAa,mBAAmB;;;;;;;AA2BhC,MAAa,oBAAoB;AACjC,MAAM,qBAAqB;CAAC;CAAU;CAAsB;CAAmB;AAAO;AAGtF,SAAgB,kBAAkB,OAA0C;CAC1E,OAAO,OAAO,UAAU,YAAa,mBAAyC,SAAS,KAAK;AAC9F;;;;;;;;;;;ACoCA,MAAa,kBAAkB;;AAE/B,MAAM,qBAAqB;AAE3B,MAAM,YAA6C;CACjD,UAAU;CACV,sBAAsB;CACtB,mBAAmB;CACnB,SAAS;AACX;;;;;AAMA,IAAa,gBAAb,cAAmC,MAAM;CAQL;CAA4B;;CAN9D;;;;;;CAMA,YAAY,KAAa,QAA4B,QAAmC;EACtF,MAAM,QAAQ,eAAe,GAAG;EAChC,MAAM,2BAA2B,MAAM,WAAW,WAAW,MACzD,6KACA,oBAAoB,SAAS,UAAU,UAAU,yFAAyF,EAAE;EAJhH,KAAA,SAAA;EAA4B,KAAA,SAAA;EAK5D,KAAK,OAAO;EACZ,KAAK,MAAM;CACb;AACF;;AAGA,IAAa,gBAAb,cAAmC,MAAM;;CAEvC,YAAY,KAAa;EACvB,MAAM,2BAA2B,eAAe,GAAG,EAAE,wLAAwL;EAC7O,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,KAAa,QAAgB;EACvC,MAAM,2BAA2B,eAAe,GAAG,EAAE,iCAAiC,OAAO,4GAA4G;EACzM,KAAK,OAAO;CACd;AACF;;AAGA,MAAM,cAAuB,OAAQ,WAAiC,QAAQ;;AAG9E,SAAS,MAAM,SAAoD;CACjE,OAAO,MAAM,KAAK,mBAAmB,KAAK,QAAQ,GAAG,IAAI,QAAQ,MAAM,aAAa,IAAI;AAC1F;AAEA,SAAS,aAAa,SAAqC;CACzD,MAAM,YAAY,CAAC,cAAc;CACjC,MAAM,UAAU,QAAQ,iBAAiB,MAAM,IAAI,WAAW;CAC9D,IAAI,QAAQ,SAAS,YAAY,eAC/B,UAAU,KAAK,GAAG,mBAAmB,QAAQ,OAAO;CACtD,MAAM,UAAU,QAAQ,SAAS,YAAY,WAAW,EAAE,eAAe,UAAU,QAAQ,QAAQ,IAAI;CACvG,MAAM,KAAK,MAAM,OAAO;CACxB,IAAI,WAAW,IAGb,OAAO,IAAIA,UAAa,QAAQ,KAAK;EAAE,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAAI;EAAW,GAAI,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC;CAAG,CAAC;CAErH,OAAO,IAAI,UAAU,QAAQ,KAAK,SAAS;AAC7C;;AAGA,IAAM,qBAAN,cAAiC,MAAM;CACrC,YAAY,KAAa,WAAmB;EAC1C,MAAM,2BAA2B,eAAe,GAAG,EAAE,mBAAmB,UAAU,KAAK;EACvF,KAAK,OAAO;CACd;AACF;AAEA,SAAS,WAAW,QAA4B;CAC9C,OAAO,OAAO,kBAAkB,QAAQ,OAAO,SAAS,IAAI,aAAa,4BAA4B,YAAY;AACnH;;AAGA,SAAS,OAAO,IAAe,KAAa,WAAmB,QAAgD;CAC7G,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,OAAO,IAAI,gBAAgB;EACjC,IAAI;EACJ,MAAM,UAAU,QAAsB;GACpC,IAAI,KAAK,OAAO,SACd;GACF,KAAK,MAAM;GACX,aAAa,KAAK;GAClB,IAAI,CAAC,KACH,OAAO,QAAQ;GACjB,GAAG,MAAM;GACT,OAAO,GAAG;EACZ;EACA,MAAM,QAAQ,iBAAyB,uBAAO,IAAI,MAAM,2BAA2B,eAAe,GAAG,EAAE,SAAS,OAAO,EAAE,CAAC;EAC1H,GAAG,iBAAiB,cAAc,OAAO,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;EACnE,GAAG,iBAAiB,SAAS,KAAK,EAAE,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;EAC9D,GAAG,iBAAiB,UAAS,UAAS,KAAK,WAAW,MAAM,OAAO,MAAM,SAAS,KAAK,MAAM,WAAW,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;EACzI,QAAQ,iBAAiB,eAAe,OAAO,WAAW,MAAM,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;EAC3F,QAAQ,iBAAiB,OAAO,IAAI,mBAAmB,KAAK,SAAS,CAAC,GAAG,SAAS;CACpF,CAAC;AACH;;AAGA,eAAe,QAAQ,SAAiD;CACtE,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;EAC/B,IAAI,WAAW,IAAI,aAAa,SAAS,WAAW;EACpD,MAAM,UAAU,YAAY,QAAQ,GAAI;EACxC,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,SAAS,QAAQ,QAAQ,EAAE,eAAe,UAAU,QAAQ,QAAQ,IAAI,CAAC;GACzE,UAAU;GACV,QAAQ,QAAQ,SAAS,YAAY,IAAI,CAAC,QAAQ,QAAQ,OAAO,CAAC,IAAI;GAEtE,GAAI,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC;EAC9B,CAAgB;EAChB,SAAc,MAAM,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;EAC3C,IAAI,SAAS,SAAS,oBAAqB,SAAS,UAAU,OAAO,SAAS,SAAS,KACrF,OAAO,IAAI,kBAAkB,QAAQ,KAAK,SAAS,MAAM;EAC3D,IAAI,SAAS,WAAW,KACtB,OAAO,IAAI,cAAc,QAAQ,GAAG;EACtC,IAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KACjD,OAAO;EACT,MAAM,SAAS,SAAS,QAAQ,IAAI,iBAAiB;EACrD,OAAO,IAAI,cAAc,QAAQ,KAAK,SAAS,QAAQ,SAAS,WAAW,OAAO,kBAAkB,MAAM,IAAI,SAAS,KAAA,CAAS;CAClI,QACM;EACJ,OAAO;CACT;AACF;;AAGA,SAAS,cAAc,QAAmB,SAAqC;CAC7E,IAAI,QAAQ,iBAAiB,KAAA,GAC3B,OAAO;CACT,MAAM,MAAM,UAAU,OAAO,KAAK;CAClC,IAAI,KAAK,OAAO,QAAQ,cACtB,OAAO;CACT,OAAO,MAAM;CACb,MAAM,IAAI,kBAAkB,eAAe,QAAQ,GAAG,GAAG,KAAK,IAAI;AACpE;;;;;;;;;;;;;;;;;;;AAoBA,eAAsB,gBAAgB,SAA8C;CAClF,MAAM,EAAE,WAAW;CACnB,QAAQ,eAAe;CACvB,MAAM,KAAK,aAAa,OAAO;CAC/B,GAAG,aAAa;CAChB,IAAI;EACF,MAAM,OAAO,IAAI,QAAQ,KAAK,QAAQ,iBAAiB,oBAAoB,MAAM;CACnF,SACO,KAAK;EACV,IAAI,QAAQ,WAAW,eAAe,oBACpC,MAAM;EACR,MAAO,MAAM,QAAQ,OAAO,KAAM;CACpC;CAEA,MAAM,UAAU,sBAAsB;CACtC,IAAI,SAAS;CAEb,MAAM,MAAM,sBAAsB;EAChC,OAAO,SAAS;GACd,IAAI;IACF,GAAG,KAAK,IAAI;GACd,QACM,CAEN;EACF;EAEA,WAAW,OAAO,YAAY;GAC5B,MAAM,QAAQ,QAAQ;GACtB,IAAI,CAAC,OACH,OAAO,cAAc,QAAQ,IAAI,kBAAkB,iCAAiC;GACtF,IAAI;IACF,OAAO,gBAAgB,QAAQ,IAAK,MAAM,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,KAAM,IAAI;GAClG,SACO,KAAK;IACV,MAAM,OAAQ,IAA2B;IACzC,OAAO,cAAc,QAAQ,IAAI,OAAO,SAAS,WAAW,OAAO,OAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;GAC7H;EACF;EACA,iBAAiB,YAAY;GAC3B,IAAI,QAAQ,WAAW,gBAAgB,CAAC,QAAQ,OAAO,cACrD,OAAO,QAAQ,eAAe,OAAO;GACvC,QAAQ,MAAM,aAAa,QAAQ,QAAS,QAAiC,MAAM;EACrF;CACF,CAAC;CAED,MAAM,UAAU,IAAI,YAAY;CAChC,GAAG,iBAAiB,YAAY,UAAwB;EACtD,IAAI,QACF;EACF,IAAI,QAAQ,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,QAAQ,OAAO,MAAM,IAAmB,CAAC;CACrG,CAAC;;CAGD,MAAM,aAAmB;EACvB,IAAI,QACF;EACF,SAAS;EACT,IAAI,MAAM;EACV,QAAQ,UAAU;CACpB;CACA,GAAG,iBAAiB,SAAS,IAAI;CACjC,GAAG,iBAAiB,eAAe,CAEnC,CAAC;CAED,MAAM,cAAoB;EACxB,IAAI,QACF;EACF,SAAS;EACT,IAAI,MAAM;EACV,GAAG,MAAM,GAAI;CACf;CAGA,MAAM,gBAAsB,MAAM;CAClC,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CACzD,IAAI;EACF,OAAO,cAAc,MAAM,cACzB;GACE;GACA;GACA,aAAa;IACX,KAAK;IACL,GAAG,MAAM,GAAI;GACf;GACA,OAAO,eAAe,QAAQ,GAAG;EACnC,GACA,QAAQ,WACR;GACE,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;GAC/D,OAAO;GACP,YAAY;GACZ,aAAa,QAAQ,eAAA;EACvB,CACF,GAAG,OAAO;CACZ,SACO,KAAK;EACV,MAAM,QAAQ,UAAU,WAAW,MAAM,IAAI;CAC/C,UACQ;EACN,QAAQ,oBAAoB,SAAS,OAAO;CAC9C;AACF"}
|