@camerontaylor/paseo-client 0.12.0-fork.1 → 0.13.0-fork.1
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/connection/index.d.ts +1 -0
- package/dist/connection/index.js +1 -0
- package/dist/daemon-client-relay-e2ee-transport.d.ts +24 -4
- package/dist/daemon-client-relay-e2ee-transport.js +94 -14
- package/dist/daemon-client-transport.d.ts +1 -1
- package/dist/daemon-client-transport.js +1 -1
- package/dist/daemon-client.d.ts +103 -9
- package/dist/daemon-client.js +329 -56
- package/dist/hosts/index.d.ts +22 -0
- package/dist/hosts/index.js +188 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +37 -6
- package/dist/node/buffering.d.ts +10 -0
- package/dist/node/buffering.js +28 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.js +188 -0
- package/dist/node/ssh-tunnel.d.ts +9 -0
- package/dist/node/ssh-tunnel.js +79 -0
- package/package.json +12 -3
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DaemonClient } from "../daemon-client.js";
|
|
2
|
+
import type { DaemonTransportFactory } from "../daemon-client-transport-types.js";
|
|
3
|
+
import type { OwnedSubscription } from "../connection/index.js";
|
|
4
|
+
import type { PaseoApi, createPaseoApi } from "../index.js";
|
|
5
|
+
import type { HostAdd, HostUpdate, HostSummary, HostDirectory } from "@camerontaylor/paseo-protocol/hosts";
|
|
6
|
+
export interface PaseoHostActions {
|
|
7
|
+
list(): Promise<HostDirectory>;
|
|
8
|
+
observe(): OwnedSubscription<HostDirectory>;
|
|
9
|
+
add(input: HostAdd): Promise<HostSummary>;
|
|
10
|
+
update(registrationId: string, patch: HostUpdate): Promise<HostSummary>;
|
|
11
|
+
remove(registrationId: string): Promise<void>;
|
|
12
|
+
client(serverId: string): Promise<PaseoApi>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createHostChannelTransport(origin: DaemonClient, serverId: string, scopeId: string, initialRouteId: string | null, link?: boolean): DaemonTransportFactory;
|
|
15
|
+
export declare function createHostActions(origin: DaemonClient, owner: {
|
|
16
|
+
own<T extends {
|
|
17
|
+
release(): Promise<void>;
|
|
18
|
+
}>(create: () => T): T;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
api: typeof createPaseoApi;
|
|
21
|
+
}): PaseoHostActions;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { safeRandomId } from "../daemon-client-transport-utils.js";
|
|
2
|
+
import { DaemonClient } from "../daemon-client.js";
|
|
3
|
+
export function createHostChannelTransport(origin, serverId, scopeId, initialRouteId, link = false) {
|
|
4
|
+
let first = initialRouteId;
|
|
5
|
+
return () => {
|
|
6
|
+
const opens = new Set();
|
|
7
|
+
const closes = new Set();
|
|
8
|
+
const messages = new Set();
|
|
9
|
+
const errors = new Set();
|
|
10
|
+
let routeId = null;
|
|
11
|
+
let closed = false;
|
|
12
|
+
const cleanups = [];
|
|
13
|
+
const close = (error) => {
|
|
14
|
+
if (closed)
|
|
15
|
+
return;
|
|
16
|
+
closed = true;
|
|
17
|
+
if (error instanceof Error)
|
|
18
|
+
Object.assign(error, { serverId });
|
|
19
|
+
for (const cleanup of cleanups)
|
|
20
|
+
cleanup();
|
|
21
|
+
try {
|
|
22
|
+
if (routeId)
|
|
23
|
+
origin.closeHostRoute(routeId);
|
|
24
|
+
else
|
|
25
|
+
origin.cancelHostRoute(scopeId);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* Transport already closed. */
|
|
29
|
+
}
|
|
30
|
+
if (error)
|
|
31
|
+
for (const handler of errors)
|
|
32
|
+
handler(error);
|
|
33
|
+
for (const handler of closes)
|
|
34
|
+
handler(error);
|
|
35
|
+
};
|
|
36
|
+
cleanups.push(origin.subscribeRawMessages((message) => {
|
|
37
|
+
if (message.type === "hosts.route.frame" && message.payload.subscriptionId === routeId) {
|
|
38
|
+
for (const handler of messages)
|
|
39
|
+
handler(message.payload.data, false);
|
|
40
|
+
}
|
|
41
|
+
if (message.type === "hosts.route.closed" && message.payload.subscriptionId === routeId)
|
|
42
|
+
close(Object.assign(new Error(message.payload.message), {
|
|
43
|
+
code: message.payload.code,
|
|
44
|
+
serverId,
|
|
45
|
+
}));
|
|
46
|
+
}));
|
|
47
|
+
cleanups.push(origin.subscribeHostFrames((frame) => {
|
|
48
|
+
if (frame.routeId === routeId)
|
|
49
|
+
for (const handler of messages)
|
|
50
|
+
handler(frame.payload, true);
|
|
51
|
+
}));
|
|
52
|
+
cleanups.push(origin.subscribeConnectionStatus((connectionState) => {
|
|
53
|
+
if (connectionState.status !== "connected")
|
|
54
|
+
close();
|
|
55
|
+
}));
|
|
56
|
+
let opening;
|
|
57
|
+
if (first)
|
|
58
|
+
opening = Promise.resolve(first);
|
|
59
|
+
else
|
|
60
|
+
opening = link ? origin.openHostLink(scopeId) : origin.openHostRoute(serverId, scopeId);
|
|
61
|
+
first = null;
|
|
62
|
+
void opening
|
|
63
|
+
.then((id) => {
|
|
64
|
+
if (closed) {
|
|
65
|
+
origin.closeHostRoute(id);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
routeId = id;
|
|
69
|
+
for (const handler of opens)
|
|
70
|
+
handler();
|
|
71
|
+
return undefined;
|
|
72
|
+
})
|
|
73
|
+
.catch(close);
|
|
74
|
+
return {
|
|
75
|
+
send(data) {
|
|
76
|
+
if (closed || !routeId)
|
|
77
|
+
throw Object.assign(new Error(`Host ${serverId} is offline`), {
|
|
78
|
+
code: "host_offline",
|
|
79
|
+
serverId,
|
|
80
|
+
});
|
|
81
|
+
origin.sendHostFrame(routeId, typeof data === "string" || data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
82
|
+
},
|
|
83
|
+
close: () => close(),
|
|
84
|
+
onOpen(handler) {
|
|
85
|
+
opens.add(handler);
|
|
86
|
+
return () => opens.delete(handler);
|
|
87
|
+
},
|
|
88
|
+
onClose(handler) {
|
|
89
|
+
closes.add(handler);
|
|
90
|
+
return () => closes.delete(handler);
|
|
91
|
+
},
|
|
92
|
+
onError(handler) {
|
|
93
|
+
errors.add(handler);
|
|
94
|
+
return () => errors.delete(handler);
|
|
95
|
+
},
|
|
96
|
+
onMessage(handler) {
|
|
97
|
+
messages.add(handler);
|
|
98
|
+
return () => messages.delete(handler);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function createHostActions(origin, owner) {
|
|
104
|
+
const assertActive = () => {
|
|
105
|
+
if (owner.signal.aborted)
|
|
106
|
+
throw new Error("Paseo API is disposed");
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
list: () => {
|
|
110
|
+
assertActive();
|
|
111
|
+
return origin.listHosts();
|
|
112
|
+
},
|
|
113
|
+
observe: () => owner.own(() => origin.observeHosts()),
|
|
114
|
+
add: (input) => {
|
|
115
|
+
assertActive();
|
|
116
|
+
return origin.addHost(input);
|
|
117
|
+
},
|
|
118
|
+
update: (id, patch) => {
|
|
119
|
+
assertActive();
|
|
120
|
+
return origin.updateHost(id, patch);
|
|
121
|
+
},
|
|
122
|
+
remove: (id) => {
|
|
123
|
+
assertActive();
|
|
124
|
+
return origin.removeHost(id);
|
|
125
|
+
},
|
|
126
|
+
async client(serverId) {
|
|
127
|
+
assertActive();
|
|
128
|
+
origin.requireConnectedHosts();
|
|
129
|
+
if (origin.getLastServerInfoMessage()?.serverId === serverId) {
|
|
130
|
+
const api = owner.api(origin, { signal: owner.signal });
|
|
131
|
+
const scope = owner.own(() => ({ release: () => api.dispose() }));
|
|
132
|
+
return { ...api, dispose: () => scope.release() };
|
|
133
|
+
}
|
|
134
|
+
const scopeId = safeRandomId();
|
|
135
|
+
const cancel = () => origin.cancelHostRoute(scopeId);
|
|
136
|
+
owner.signal.addEventListener("abort", cancel, { once: true });
|
|
137
|
+
let routeId;
|
|
138
|
+
try {
|
|
139
|
+
routeId = await origin.openHostRoute(serverId, scopeId);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), {
|
|
143
|
+
serverId,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
owner.signal.removeEventListener("abort", cancel);
|
|
148
|
+
}
|
|
149
|
+
if (owner.signal.aborted) {
|
|
150
|
+
origin.closeHostRoute(routeId);
|
|
151
|
+
assertActive();
|
|
152
|
+
}
|
|
153
|
+
const client = new DaemonClient({
|
|
154
|
+
url: "ws://routed-host/ws",
|
|
155
|
+
clientId: scopeId,
|
|
156
|
+
transportFactory: createHostChannelTransport(origin, serverId, scopeId, routeId),
|
|
157
|
+
// Initial acquisition has a deadline; established scopes restore demand on loss.
|
|
158
|
+
reconnect: { enabled: false },
|
|
159
|
+
});
|
|
160
|
+
const api = owner.api(client, { signal: owner.signal });
|
|
161
|
+
const scope = owner.own(() => ({
|
|
162
|
+
release: async () => {
|
|
163
|
+
try {
|
|
164
|
+
await api.dispose();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await client.close();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
try {
|
|
172
|
+
await client.connect();
|
|
173
|
+
client.setReconnectEnabled(true);
|
|
174
|
+
assertActive();
|
|
175
|
+
return { ...api, dispose: () => scope.release() };
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const failure = Object.assign(error instanceof Error ? error : new Error("Host handshake failed"), {
|
|
179
|
+
code: error instanceof Error && "code" in error ? error.code : "host_offline",
|
|
180
|
+
serverId,
|
|
181
|
+
});
|
|
182
|
+
await scope.release();
|
|
183
|
+
throw failure;
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { AgentAddress } from "@camerontaylor/paseo-protocol/addresses";
|
|
2
|
+
export type { AgentAddress } from "@camerontaylor/paseo-protocol/addresses";
|
|
3
|
+
import { type PaseoHostActions } from "./hosts/index.js";
|
|
4
|
+
export type { PaseoHostActions } from "./hosts/index.js";
|
|
5
|
+
export type { HostAdd, HostUpdate, HostSummary, HostDirectory } from "@camerontaylor/paseo-protocol/hosts";
|
|
1
6
|
import type { OwnedSubscription } from "./connection/index.js";
|
|
2
7
|
export type { OwnedSubscription, SubscriptionObserver } from "./connection/index.js";
|
|
3
8
|
import type { DaemonClientConfig } from "./daemon-client.js";
|
|
@@ -167,7 +172,7 @@ export interface PaseoAgentCreateOptions {
|
|
|
167
172
|
onEvent?: (snapshot: CreationSnapshot) => void;
|
|
168
173
|
config: PaseoAgentConfig;
|
|
169
174
|
cwd: string;
|
|
170
|
-
parent?: string | PaseoAgentHandle;
|
|
175
|
+
parent?: string | AgentAddress | PaseoAgentHandle;
|
|
171
176
|
title?: PaseoAgentSessionConfig["title"];
|
|
172
177
|
env?: CreateAgentRequestMessage["env"];
|
|
173
178
|
prompt?: string;
|
|
@@ -259,6 +264,7 @@ export interface PaseoAgentTimelineHandle {
|
|
|
259
264
|
}
|
|
260
265
|
export interface PaseoAgentHandle {
|
|
261
266
|
readonly id: string;
|
|
267
|
+
readonly address: AgentAddress;
|
|
262
268
|
/**
|
|
263
269
|
* `workspaceId` through `archivedAt` mirror the last snapshot this handle
|
|
264
270
|
* observed. A handle from `ref()` reads `null` for all of them until
|
|
@@ -390,6 +396,7 @@ export interface PaseoConfigActions {
|
|
|
390
396
|
}>;
|
|
391
397
|
}
|
|
392
398
|
export interface PaseoApi {
|
|
399
|
+
readonly hosts: PaseoHostActions;
|
|
393
400
|
dispose(): Promise<void>;
|
|
394
401
|
observeEvents: DaemonClient["observeEvents"];
|
|
395
402
|
readonly terminals: PaseoTerminalActions;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parentAddressLabels } from "@camerontaylor/paseo-protocol/agent-labels";
|
|
2
|
+
import { createHostActions } from "./hosts/index.js";
|
|
1
3
|
import { DaemonClient } from "./daemon-client.js";
|
|
2
4
|
import { createTerminalActions, } from "./terminals/index.js";
|
|
3
5
|
/**
|
|
@@ -27,10 +29,11 @@ export function createPaseoClient(config) {
|
|
|
27
29
|
getConnectionState: () => daemonClient.getConnectionState(),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
|
-
function toDaemonAgentCreateOptions(options, placement) {
|
|
32
|
+
function toDaemonAgentCreateOptions(options, placement, serverId) {
|
|
31
33
|
const { config: agentConfig, cwd, parent, title, prompt, ...requestOptions } = options;
|
|
32
34
|
const { provider: providerModel, options: providerOptions, ...runtimeConfig } = agentConfig;
|
|
33
35
|
const { provider, model } = parseProviderModel(providerModel);
|
|
36
|
+
const relationship = parent ? encodeParent(parent, serverId) : {};
|
|
34
37
|
return {
|
|
35
38
|
...requestOptions,
|
|
36
39
|
config: {
|
|
@@ -42,7 +45,12 @@ function toDaemonAgentCreateOptions(options, placement) {
|
|
|
42
45
|
...(providerOptions !== undefined ? { providerOptions } : {}),
|
|
43
46
|
},
|
|
44
47
|
...(placement ? { workspaceId: placement.workspaceId } : {}),
|
|
45
|
-
...(parent
|
|
48
|
+
...(parent
|
|
49
|
+
? {
|
|
50
|
+
...relationship,
|
|
51
|
+
labels: { ...requestOptions.labels, ...relationship.labels },
|
|
52
|
+
}
|
|
53
|
+
: {}),
|
|
46
54
|
...(prompt !== undefined ? { initialPrompt: prompt } : {}),
|
|
47
55
|
};
|
|
48
56
|
}
|
|
@@ -81,7 +89,7 @@ export function createPaseoApi(daemonClient, scopeOptions) {
|
|
|
81
89
|
};
|
|
82
90
|
const createAgentHandle = createAgentHandleFactory(daemonClient, listenAgents, (agentId, handler) => own(() => daemonClient.subscribeAgentTimeline(agentId, handler)));
|
|
83
91
|
const createAgent = async (options, placement) => {
|
|
84
|
-
const agent = await daemonClient.createAgent(toDaemonAgentCreateOptions(options, placement));
|
|
92
|
+
const agent = await daemonClient.createAgent(toDaemonAgentCreateOptions(options, placement, daemonClient.getLastServerInfoMessage()?.serverId));
|
|
85
93
|
return createAgentHandle(agent);
|
|
86
94
|
};
|
|
87
95
|
const terminals = createTerminalActions(daemonClient, async (workspaceId) => {
|
|
@@ -160,6 +168,7 @@ export function createPaseoApi(daemonClient, scopeOptions) {
|
|
|
160
168
|
}
|
|
161
169
|
return {
|
|
162
170
|
dispose,
|
|
171
|
+
hosts: createHostActions(daemonClient, { own, signal: lifetime.signal, api: createPaseoApi }),
|
|
163
172
|
observeEvents,
|
|
164
173
|
terminals,
|
|
165
174
|
projects: {
|
|
@@ -176,7 +185,14 @@ export function createPaseoApi(daemonClient, scopeOptions) {
|
|
|
176
185
|
ref: (workspace) => createWorkspaceHandle(workspace),
|
|
177
186
|
open: (input, requestId) => openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
|
|
178
187
|
create: async ({ requestId, agent, ...options }) => {
|
|
179
|
-
const result = await daemonClient.createWorkspace({
|
|
188
|
+
const result = await daemonClient.createWorkspace({
|
|
189
|
+
...options,
|
|
190
|
+
...(agent
|
|
191
|
+
? {
|
|
192
|
+
agent: toDaemonAgentCreateOptions(agent, undefined, daemonClient.getLastServerInfoMessage()?.serverId),
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
}, requestId);
|
|
180
196
|
if (result.error || !result.workspace) {
|
|
181
197
|
throw new Error(result.error ?? "The daemon did not create a workspace");
|
|
182
198
|
}
|
|
@@ -296,8 +312,15 @@ function createAgentHandleFactory(daemonClient, listen, subscribeTimeline) {
|
|
|
296
312
|
return (agent) => {
|
|
297
313
|
const id = typeof agent === "string" ? agent : agent.id;
|
|
298
314
|
let current = typeof agent === "string" ? null : agent;
|
|
315
|
+
let serverId = daemonClient.getLastServerInfoMessage()?.serverId;
|
|
299
316
|
const handle = {
|
|
300
317
|
id,
|
|
318
|
+
get address() {
|
|
319
|
+
serverId ?? (serverId = daemonClient.getLastServerInfoMessage()?.serverId);
|
|
320
|
+
if (!serverId)
|
|
321
|
+
throw new Error("Connect to a host before obtaining an agent address");
|
|
322
|
+
return { serverId, agentId: id };
|
|
323
|
+
},
|
|
301
324
|
timeline: {
|
|
302
325
|
append: (item) => daemonClient.appendAgentTimelineItem(id, item),
|
|
303
326
|
refetch: async (options) => {
|
|
@@ -429,8 +452,15 @@ async function openWorkspace(daemonClient, createWorkspaceHandle, input, request
|
|
|
429
452
|
function resolveWorkspaceId(workspace) {
|
|
430
453
|
return typeof workspace === "string" ? workspace : workspace.id;
|
|
431
454
|
}
|
|
432
|
-
function
|
|
433
|
-
|
|
455
|
+
function encodeParent(parent, serverId) {
|
|
456
|
+
if (typeof parent === "string")
|
|
457
|
+
return { callerAgentId: parent };
|
|
458
|
+
const address = "address" in parent ? parent.address : parent;
|
|
459
|
+
if (!serverId)
|
|
460
|
+
throw new Error("Connect to a host before creating a child");
|
|
461
|
+
return address.serverId === serverId
|
|
462
|
+
? { callerAgentId: address.agentId, labels: parentAddressLabels(address) }
|
|
463
|
+
: { labels: parentAddressLabels(address) };
|
|
434
464
|
}
|
|
435
465
|
function parseProviderModel(selection) {
|
|
436
466
|
const separator = selection.indexOf("/");
|
|
@@ -443,6 +473,7 @@ function parseProviderModel(selection) {
|
|
|
443
473
|
};
|
|
444
474
|
}
|
|
445
475
|
function listProviderUsage(daemonClient, options) {
|
|
476
|
+
daemonClient.ensureConnected();
|
|
446
477
|
// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-02-28 once daemon floor >= v0.1.98.
|
|
447
478
|
if (daemonClient.getLastServerInfoMessage()?.features?.providerUsageList !== true) {
|
|
448
479
|
return Promise.reject(new Error("Update the host to list provider usage."));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Counts bytes until the socket's send callback, shared by all links owned by a daemon. */
|
|
2
|
+
export declare class HostTransportBudget {
|
|
3
|
+
readonly limit: number;
|
|
4
|
+
private used;
|
|
5
|
+
constructor(limit?: number);
|
|
6
|
+
get bufferedBytes(): number;
|
|
7
|
+
assertCapacity(bytes: number): void;
|
|
8
|
+
reserve(bytes: number): () => void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=buffering.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Counts bytes until the socket's send callback, shared by all links owned by a daemon. */
|
|
2
|
+
export class HostTransportBudget {
|
|
3
|
+
constructor(limit = 256 * 1024 * 1024) {
|
|
4
|
+
this.limit = limit;
|
|
5
|
+
this.used = 0;
|
|
6
|
+
}
|
|
7
|
+
get bufferedBytes() {
|
|
8
|
+
return this.used;
|
|
9
|
+
}
|
|
10
|
+
assertCapacity(bytes) {
|
|
11
|
+
if (bytes > this.limit - this.used)
|
|
12
|
+
throw Object.assign(new Error("Connected host buffer limit reached"), {
|
|
13
|
+
code: "resource_limit",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
reserve(bytes) {
|
|
17
|
+
this.assertCapacity(bytes);
|
|
18
|
+
this.used += bytes;
|
|
19
|
+
let released = false;
|
|
20
|
+
return () => {
|
|
21
|
+
if (!released) {
|
|
22
|
+
released = true;
|
|
23
|
+
this.used -= bytes;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=buffering.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { createHostChannelTransport } from "../hosts/index.js";
|
|
2
|
+
import { HostTransportBudget } from "./buffering.js";
|
|
3
|
+
export { HostTransportBudget } from "./buffering.js";
|
|
4
|
+
import { parseHostTarget } from "@camerontaylor/paseo-protocol/host-target";
|
|
5
|
+
import type { DaemonTransportFactory } from "../daemon-client-transport-types.js";
|
|
6
|
+
import { DaemonClient, type DaemonClientConfig } from "../daemon-client.js";
|
|
7
|
+
export { parseHostTarget };
|
|
8
|
+
export type { SshTunnel } from "./ssh-tunnel.js";
|
|
9
|
+
export { createSshTunnel, resolveSshFailureDetail } from "./ssh-tunnel.js";
|
|
10
|
+
/** A factory owns one attempt, including a fresh SSH process and its teardown. */
|
|
11
|
+
export declare function createHostTransportFactory(connection: string, budget?: HostTransportBudget): DaemonTransportFactory;
|
|
12
|
+
export declare function createHostClient(connection: string, config: Omit<DaemonClientConfig, "url" | "transportFactory">, budget?: HostTransportBudget): DaemonClient;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export { createHostChannelTransport } from "../hosts/index.js";
|
|
2
|
+
import { HostTransportBudget } from "./buffering.js";
|
|
3
|
+
export { HostTransportBudget } from "./buffering.js";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { WebSocket } from "ws";
|
|
6
|
+
import { parseHostTarget } from "@camerontaylor/paseo-protocol/host-target";
|
|
7
|
+
import { createSshTunnel } from "./ssh-tunnel.js";
|
|
8
|
+
import { createWebSocketTransportFactory } from "../daemon-client-websocket-transport.js";
|
|
9
|
+
import { createRelayE2eeTransportFactory } from "../daemon-client-relay-e2ee-transport.js";
|
|
10
|
+
import { DaemonClient } from "../daemon-client.js";
|
|
11
|
+
export { parseHostTarget };
|
|
12
|
+
export { createSshTunnel, resolveSshFailureDetail } from "./ssh-tunnel.js";
|
|
13
|
+
/** A factory owns one attempt, including a fresh SSH process and its teardown. */
|
|
14
|
+
export function createHostTransportFactory(connection, budget = new HostTransportBudget()) {
|
|
15
|
+
const target = parseHostTarget(connection);
|
|
16
|
+
// Rises to "protected" once the daemon negotiates it, so a later attempt
|
|
17
|
+
// cannot be downgraded to legacy endpoint security.
|
|
18
|
+
let securityFloor = target.kind === "socket" ? target.offer?.minimumSecurityMode : undefined;
|
|
19
|
+
return (attemptOptions) => {
|
|
20
|
+
const opens = new Set();
|
|
21
|
+
const closes = new Set();
|
|
22
|
+
const errors = new Set();
|
|
23
|
+
const messages = new Set();
|
|
24
|
+
let closed = false;
|
|
25
|
+
let closeEvent;
|
|
26
|
+
let transport;
|
|
27
|
+
let teardown = () => { };
|
|
28
|
+
let disposeSocket = () => { };
|
|
29
|
+
let assertOutboundCapacity = (bytes) => budget.assertCapacity(bytes);
|
|
30
|
+
let sshTunnel = null;
|
|
31
|
+
const close = (code, reason) => {
|
|
32
|
+
if (closed)
|
|
33
|
+
return;
|
|
34
|
+
closed = true;
|
|
35
|
+
try {
|
|
36
|
+
transport?.close(code, reason);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
try {
|
|
40
|
+
disposeSocket();
|
|
41
|
+
teardown();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
for (const handler of closes)
|
|
45
|
+
handler(closeEvent);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
void (async () => {
|
|
50
|
+
let socketTarget;
|
|
51
|
+
if (target.kind === "ssh") {
|
|
52
|
+
const tunnel = await createSshTunnel(target.ssh);
|
|
53
|
+
sshTunnel = tunnel;
|
|
54
|
+
teardown = () => tunnel.close();
|
|
55
|
+
socketTarget = parseHostTarget(tunnel.endpoint);
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
socketTarget = target;
|
|
59
|
+
if (closed) {
|
|
60
|
+
teardown();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const socketPath = socketTarget.socketPath?.replace(/^~(?=\/)/, homedir());
|
|
64
|
+
let factory = createWebSocketTransportFactory((url, options) => {
|
|
65
|
+
const socket = new WebSocket(socketPath && url.startsWith("ws+unix:") ? `ws+unix://${socketPath}:/ws` : url, options?.protocols, {
|
|
66
|
+
headers: options?.headers,
|
|
67
|
+
...(socketPath ? { socketPath } : {}),
|
|
68
|
+
maxPayload: 100 * 1024 * 1024,
|
|
69
|
+
});
|
|
70
|
+
disposeSocket = () => socket.terminate();
|
|
71
|
+
assertOutboundCapacity = (bytes) => {
|
|
72
|
+
if (socket.bufferedAmount + bytes > 128 * 1024 * 1024) {
|
|
73
|
+
throw Object.assign(new Error("Host socket buffer limit reached"), {
|
|
74
|
+
code: "resource_limit",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
budget.assertCapacity(bytes);
|
|
78
|
+
};
|
|
79
|
+
const pending = new Set();
|
|
80
|
+
const send = socket.send.bind(socket);
|
|
81
|
+
socket.send = (data, ...args) => {
|
|
82
|
+
const bytes = typeof data === "string"
|
|
83
|
+
? Buffer.byteLength(data)
|
|
84
|
+
: Buffer.byteLength(data);
|
|
85
|
+
assertOutboundCapacity(bytes);
|
|
86
|
+
const release = budget.reserve(bytes);
|
|
87
|
+
pending.add(release);
|
|
88
|
+
const callback = args.find((value) => typeof value === "function");
|
|
89
|
+
const sendOptions = args.find((value) => value !== null && typeof value === "object");
|
|
90
|
+
try {
|
|
91
|
+
send(data, sendOptions ?? {}, (error) => {
|
|
92
|
+
release();
|
|
93
|
+
pending.delete(release);
|
|
94
|
+
callback?.(error);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
release();
|
|
99
|
+
pending.delete(release);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
socket.once("close", () => {
|
|
104
|
+
for (const release of pending)
|
|
105
|
+
release();
|
|
106
|
+
pending.clear();
|
|
107
|
+
});
|
|
108
|
+
return socket;
|
|
109
|
+
});
|
|
110
|
+
if (socketTarget.offer)
|
|
111
|
+
factory = createRelayE2eeTransportFactory({
|
|
112
|
+
baseFactory: factory,
|
|
113
|
+
daemonPublicKeyB64: socketTarget.offer.daemonPublicKeyB64,
|
|
114
|
+
...(securityFloor ? { minimumMode: securityFloor } : {}),
|
|
115
|
+
onSecurityMode: (mode) => {
|
|
116
|
+
if (mode === "protected")
|
|
117
|
+
securityFloor = "protected";
|
|
118
|
+
},
|
|
119
|
+
logger: { warn() { } },
|
|
120
|
+
assertOutboundCapacity: (bytes) => assertOutboundCapacity(bytes),
|
|
121
|
+
});
|
|
122
|
+
transport = factory({ ...attemptOptions, url: socketTarget.url });
|
|
123
|
+
transport.onOpen(() => {
|
|
124
|
+
if (!closed)
|
|
125
|
+
for (const handler of opens)
|
|
126
|
+
handler();
|
|
127
|
+
});
|
|
128
|
+
transport.onClose((event) => {
|
|
129
|
+
closeEvent = event;
|
|
130
|
+
close();
|
|
131
|
+
});
|
|
132
|
+
transport.onError((error) => {
|
|
133
|
+
if (closed)
|
|
134
|
+
return;
|
|
135
|
+
const detail = sshTunnel?.failureDetail() ?? "";
|
|
136
|
+
const failure = /permission denied|authentication failed|host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(detail)
|
|
137
|
+
? Object.assign(new Error("SSH authentication or host verification failed. Check the SSH configuration on this daemon."), { code: "authentication_failed" })
|
|
138
|
+
: error;
|
|
139
|
+
for (const handler of errors)
|
|
140
|
+
handler(failure);
|
|
141
|
+
close();
|
|
142
|
+
});
|
|
143
|
+
transport.onMessage((data, binary) => {
|
|
144
|
+
if (!closed)
|
|
145
|
+
for (const handler of messages)
|
|
146
|
+
handler(data, binary);
|
|
147
|
+
});
|
|
148
|
+
})().catch(() => {
|
|
149
|
+
for (const handler of errors)
|
|
150
|
+
handler(new Error("Host transport failed"));
|
|
151
|
+
close();
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
send(data) {
|
|
155
|
+
if (closed || !transport)
|
|
156
|
+
throw new Error("Host transport is unavailable");
|
|
157
|
+
transport.send(data);
|
|
158
|
+
},
|
|
159
|
+
close,
|
|
160
|
+
onOpen(handler) {
|
|
161
|
+
opens.add(handler);
|
|
162
|
+
return () => opens.delete(handler);
|
|
163
|
+
},
|
|
164
|
+
onClose(handler) {
|
|
165
|
+
closes.add(handler);
|
|
166
|
+
return () => closes.delete(handler);
|
|
167
|
+
},
|
|
168
|
+
onError(handler) {
|
|
169
|
+
errors.add(handler);
|
|
170
|
+
return () => errors.delete(handler);
|
|
171
|
+
},
|
|
172
|
+
onMessage(handler) {
|
|
173
|
+
messages.add(handler);
|
|
174
|
+
return () => messages.delete(handler);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function createHostClient(connection, config, budget) {
|
|
180
|
+
const target = parseHostTarget(connection);
|
|
181
|
+
return new DaemonClient({
|
|
182
|
+
...config,
|
|
183
|
+
url: "ws://host-connector/ws",
|
|
184
|
+
...(target.password ? { password: target.password } : {}),
|
|
185
|
+
transportFactory: createHostTransportFactory(connection, budget),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SshTransportTarget } from "@camerontaylor/paseo-protocol/ssh-transport";
|
|
2
|
+
export interface SshTunnel {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
close(): void;
|
|
5
|
+
failureDetail(): string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveSshFailureDetail(failure: string | null, stderr: string): string | null;
|
|
8
|
+
export declare function createSshTunnel(target: SshTransportTarget): Promise<SshTunnel>;
|
|
9
|
+
//# sourceMappingURL=ssh-tunnel.d.ts.map
|