@rome-os/node-core 0.1.0

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/bin/daemon.js +2 -0
  4. package/dist/actions.d.ts +20 -0
  5. package/dist/actions.js +18 -0
  6. package/dist/auth.d.ts +3 -0
  7. package/dist/auth.js +76 -0
  8. package/dist/client.d.ts +28 -0
  9. package/dist/client.js +130 -0
  10. package/dist/cloud.d.ts +7 -0
  11. package/dist/cloud.js +56 -0
  12. package/dist/connect.d.ts +27 -0
  13. package/dist/connect.js +115 -0
  14. package/dist/connector.d.ts +26 -0
  15. package/dist/connector.js +155 -0
  16. package/dist/daemon-client.d.ts +37 -0
  17. package/dist/daemon-client.js +244 -0
  18. package/dist/daemon-control.d.ts +13 -0
  19. package/dist/daemon-control.js +151 -0
  20. package/dist/daemon-entry.d.ts +1 -0
  21. package/dist/daemon-entry.js +16 -0
  22. package/dist/daemon-protocol.d.ts +16 -0
  23. package/dist/daemon-protocol.js +15 -0
  24. package/dist/daemon-rpc.d.ts +34 -0
  25. package/dist/daemon-rpc.js +171 -0
  26. package/dist/daemon.d.ts +2 -0
  27. package/dist/daemon.js +235 -0
  28. package/dist/devices-status.d.ts +23 -0
  29. package/dist/devices-status.js +68 -0
  30. package/dist/executor.d.ts +5 -0
  31. package/dist/executor.js +105 -0
  32. package/dist/gateway-url.d.ts +1 -0
  33. package/dist/gateway-url.js +11 -0
  34. package/dist/host.d.ts +3 -0
  35. package/dist/host.js +3 -0
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.js +4 -0
  38. package/dist/local.d.ts +21 -0
  39. package/dist/local.js +47 -0
  40. package/dist/login.d.ts +6 -0
  41. package/dist/login.js +107 -0
  42. package/dist/protocol.d.ts +29 -0
  43. package/dist/protocol.js +39 -0
  44. package/dist/socket.d.ts +2 -0
  45. package/dist/socket.js +25 -0
  46. package/dist/storage.d.ts +8 -0
  47. package/dist/storage.js +55 -0
  48. package/package.json +64 -0
@@ -0,0 +1,34 @@
1
+ import WebSocket from "ws";
2
+ import { type DaemonStatus } from "./daemon-protocol.js";
3
+ export declare class DaemonVersionError extends Error {
4
+ constructor();
5
+ }
6
+ export declare class DaemonRequestError extends Error {
7
+ readonly code = "unknown_outcome";
8
+ constructor();
9
+ }
10
+ export declare class RpcError extends Error {
11
+ readonly code: number;
12
+ constructor(code: number, message: string);
13
+ }
14
+ export interface DaemonState {
15
+ token: string;
16
+ port: number;
17
+ pid: number;
18
+ protocolVersion?: number;
19
+ }
20
+ export declare class RpcConnection {
21
+ readonly socket: WebSocket;
22
+ private nextId;
23
+ private pending;
24
+ onNotification?: (method: string, params: unknown) => void;
25
+ onClose?: () => void;
26
+ constructor(socket: WebSocket);
27
+ get open(): boolean;
28
+ request(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;
29
+ close(): void;
30
+ }
31
+ export declare function connectRpc(state: DaemonState, signal?: AbortSignal): Promise<{
32
+ rpc: RpcConnection;
33
+ status: DaemonStatus;
34
+ }>;
@@ -0,0 +1,171 @@
1
+ import WebSocket from "ws";
2
+ import { isRecord } from "./actions.js";
3
+ import { DAEMON_PROTOCOL_VERSION, parseDaemonStatus, } from "./daemon-protocol.js";
4
+ export class DaemonVersionError extends Error {
5
+ constructor() {
6
+ super("The caller daemon uses an incompatible protocol. Stop it explicitly before restarting with this version.");
7
+ }
8
+ }
9
+ export class DaemonRequestError extends Error {
10
+ code = "unknown_outcome";
11
+ constructor() {
12
+ super("Caller daemon connection failed. Execution outcome may be unknown; do not automatically retry.");
13
+ }
14
+ }
15
+ export class RpcError extends Error {
16
+ code;
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.code = code;
20
+ }
21
+ }
22
+ export class RpcConnection {
23
+ socket;
24
+ nextId = 0;
25
+ pending = new Map();
26
+ onNotification;
27
+ onClose;
28
+ constructor(socket) {
29
+ this.socket = socket;
30
+ let alive = true;
31
+ const heartbeat = setInterval(() => {
32
+ if (!this.open)
33
+ return;
34
+ if (!alive) {
35
+ socket.terminate();
36
+ return;
37
+ }
38
+ alive = false;
39
+ socket.ping();
40
+ }, 30_000);
41
+ heartbeat.unref();
42
+ socket.on("pong", () => {
43
+ alive = true;
44
+ });
45
+ socket.on("error", () => socket.terminate());
46
+ socket.on("close", () => {
47
+ clearInterval(heartbeat);
48
+ for (const request of this.pending.values())
49
+ request.reject(new DaemonRequestError());
50
+ this.pending.clear();
51
+ this.onClose?.();
52
+ });
53
+ socket.on("message", (data, binary) => {
54
+ let message;
55
+ try {
56
+ message = JSON.parse(data.toString());
57
+ }
58
+ catch {
59
+ this.close();
60
+ return;
61
+ }
62
+ if (binary || !isRecord(message) || message.jsonrpc !== "2.0") {
63
+ this.close();
64
+ return;
65
+ }
66
+ if (typeof message.method === "string" && !Object.hasOwn(message, "id")) {
67
+ this.onNotification?.(message.method, message.params);
68
+ return;
69
+ }
70
+ const pending = typeof message.id === "string" ? this.pending.get(message.id) : undefined;
71
+ if (!pending)
72
+ return;
73
+ if (Object.hasOwn(message, "result") && !Object.hasOwn(message, "error"))
74
+ pending.resolve(message.result);
75
+ else if (!Object.hasOwn(message, "result") &&
76
+ isRecord(message.error) &&
77
+ Number.isInteger(message.error.code) &&
78
+ typeof message.error.message === "string")
79
+ pending.reject(new RpcError(Number(message.error.code), message.error.message));
80
+ else
81
+ pending.reject(new DaemonRequestError());
82
+ });
83
+ }
84
+ get open() {
85
+ return this.socket.readyState === WebSocket.OPEN;
86
+ }
87
+ request(method, params = {}, timeoutMs = 85_000) {
88
+ if (!this.open)
89
+ return Promise.reject(new DaemonRequestError());
90
+ const id = String(++this.nextId);
91
+ const text = JSON.stringify({ jsonrpc: "2.0", id, method, params });
92
+ return new Promise((resolve, reject) => {
93
+ const finish = (error, value) => {
94
+ clearTimeout(timer);
95
+ this.pending.delete(id);
96
+ if (error)
97
+ reject(error);
98
+ else
99
+ resolve(value);
100
+ };
101
+ const timer = setTimeout(() => finish(new DaemonRequestError()), timeoutMs);
102
+ this.pending.set(id, {
103
+ resolve: (value) => finish(undefined, value),
104
+ reject: (error) => finish(error),
105
+ });
106
+ try {
107
+ this.socket.send(text, (error) => {
108
+ if (error)
109
+ finish(new DaemonRequestError());
110
+ });
111
+ }
112
+ catch {
113
+ finish(new DaemonRequestError());
114
+ }
115
+ });
116
+ }
117
+ close() {
118
+ this.socket.terminate();
119
+ }
120
+ }
121
+ export async function connectRpc(state, signal) {
122
+ signal?.throwIfAborted();
123
+ const socket = new WebSocket(`ws://127.0.0.1:${state.port}/rpc`, {
124
+ headers: { authorization: `Bearer ${state.token}` },
125
+ handshakeTimeout: 1500,
126
+ perMessageDeflate: false,
127
+ followRedirects: false,
128
+ });
129
+ const rpc = new RpcConnection(socket);
130
+ const abort = () => rpc.close();
131
+ signal?.addEventListener("abort", abort, { once: true });
132
+ try {
133
+ await new Promise((resolve, reject) => {
134
+ const cleanup = () => {
135
+ socket.off("open", opened);
136
+ socket.off("close", failed);
137
+ socket.off("error", failed);
138
+ };
139
+ const opened = () => {
140
+ cleanup();
141
+ resolve();
142
+ };
143
+ const failed = () => {
144
+ cleanup();
145
+ reject(new DaemonRequestError());
146
+ };
147
+ socket.once("open", opened);
148
+ socket.once("close", failed);
149
+ socket.once("error", failed);
150
+ if (signal?.aborted)
151
+ abort();
152
+ });
153
+ const value = await rpc.request("daemon.hello", { protocolVersion: DAEMON_PROTOCOL_VERSION }, 1500);
154
+ if (isRecord(value) && value.protocolVersion !== DAEMON_PROTOCOL_VERSION)
155
+ throw new DaemonVersionError();
156
+ const status = parseDaemonStatus(value);
157
+ if (!status || status.pid !== state.pid)
158
+ throw new DaemonRequestError();
159
+ signal?.throwIfAborted();
160
+ return { rpc, status };
161
+ }
162
+ catch (error) {
163
+ rpc.close();
164
+ if (error instanceof RpcError && error.code === -32002)
165
+ throw new DaemonVersionError();
166
+ throw error;
167
+ }
168
+ finally {
169
+ signal?.removeEventListener("abort", abort);
170
+ }
171
+ }
@@ -0,0 +1,2 @@
1
+ import { type NodeConfig } from "./local.js";
2
+ export declare function serveDaemon(config: NodeConfig, signal: AbortSignal): Promise<void>;
package/dist/daemon.js ADDED
@@ -0,0 +1,235 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import WebSocket, { WebSocketServer } from "ws";
4
+ import { isRecord } from "./actions.js";
5
+ import { DeviceConnector } from "./connector.js";
6
+ import { daemonStatePath, readCallerCredential } from "./local.js";
7
+ import { writePrivateJson } from "./storage.js";
8
+ import { DAEMON_PROTOCOL_VERSION } from "./daemon-protocol.js";
9
+ import { DeviceStatusReader } from "./devices-status.js";
10
+ import { RpcError } from "./daemon-rpc.js";
11
+ export async function serveDaemon(config, signal) {
12
+ if (signal.aborted)
13
+ return;
14
+ const credential = await readCallerCredential(config);
15
+ const peers = new Set();
16
+ const state = {
17
+ port: config.port,
18
+ pid: process.pid,
19
+ protocolVersion: DAEMON_PROTOCOL_VERSION,
20
+ token: `romenode_${randomBytes(32).toString("base64url")}`,
21
+ };
22
+ let closing = false;
23
+ const connector = new DeviceConnector({
24
+ credential,
25
+ onStatus: () => {
26
+ deviceStatus.invalidate();
27
+ for (const peer of peers)
28
+ if (peer.subscribed)
29
+ notify(peer);
30
+ },
31
+ });
32
+ const deviceStatus = new DeviceStatusReader(connector);
33
+ const snapshot = () => ({
34
+ pid: state.pid,
35
+ protocolVersion: DAEMON_PROTOCOL_VERSION,
36
+ connection: connector.getStatus(),
37
+ });
38
+ function authorized(req) {
39
+ const actual = Buffer.from(req.headers.authorization ?? "");
40
+ const expected = Buffer.from(`Bearer ${state.token}`);
41
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
42
+ }
43
+ const server = createServer((req, res) => {
44
+ res.writeHead(authorized(req) ? 426 : 401, { "cache-control": "no-store" }).end();
45
+ });
46
+ server.headersTimeout = 10_000;
47
+ const sockets = new WebSocketServer({ noServer: true, perMessageDeflate: false });
48
+ server.on("upgrade", (req, socket, head) => {
49
+ if (closing || req.url !== "/rpc" || req.headers.origin || !authorized(req)) {
50
+ socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
51
+ return;
52
+ }
53
+ sockets.handleUpgrade(req, socket, head, (socket) => sockets.emit("connection", socket));
54
+ });
55
+ function send(peer, value) {
56
+ if (peer.socket.readyState !== WebSocket.OPEN)
57
+ return;
58
+ peer.socket.send(JSON.stringify(value), (error) => {
59
+ if (error)
60
+ peer.socket.terminate();
61
+ });
62
+ }
63
+ function notify(peer) {
64
+ if (peer.socket.bufferedAmount > 1024 * 1024) {
65
+ peer.socket.terminate();
66
+ return;
67
+ }
68
+ send(peer, { jsonrpc: "2.0", method: "events.connection", params: snapshot() });
69
+ }
70
+ const close = () => {
71
+ if (closing)
72
+ return;
73
+ closing = true;
74
+ connector.stop();
75
+ for (const peer of peers)
76
+ peer.socket.close(1001, "daemon stopped");
77
+ server.close();
78
+ sockets.close();
79
+ const timer = setTimeout(() => {
80
+ for (const peer of peers)
81
+ peer.socket.terminate();
82
+ server.closeAllConnections();
83
+ }, 1000);
84
+ timer.unref();
85
+ };
86
+ function error(id, code, message) {
87
+ return { jsonrpc: "2.0", id, error: { code, message } };
88
+ }
89
+ async function dispatch(peer, value) {
90
+ if (!isRecord(value) ||
91
+ value.jsonrpc !== "2.0" ||
92
+ typeof value.method !== "string" ||
93
+ (Object.hasOwn(value, "id") &&
94
+ typeof value.id !== "string" &&
95
+ typeof value.id !== "number" &&
96
+ value.id !== null))
97
+ return error(null, -32600, "Invalid Request");
98
+ const hasId = Object.hasOwn(value, "id");
99
+ const id = value.id;
100
+ if (hasId && id !== null && peer.pending.has(id)) {
101
+ peer.socket.close(1008, "duplicate request ID");
102
+ return;
103
+ }
104
+ if (hasId && id !== null)
105
+ peer.pending.add(id);
106
+ try {
107
+ if (closing)
108
+ throw new RpcError(-32000, "The caller daemon is stopping.");
109
+ const params = Object.hasOwn(value, "params") ? value.params : {};
110
+ if (!isRecord(params))
111
+ throw new RpcError(-32602, "Invalid params");
112
+ let result;
113
+ if (value.method === "daemon.hello") {
114
+ if (params.protocolVersion !== DAEMON_PROTOCOL_VERSION)
115
+ throw new RpcError(-32002, "Incompatible daemon protocol.");
116
+ peer.ready = true;
117
+ result = snapshot();
118
+ }
119
+ else {
120
+ if (!peer.ready)
121
+ throw new RpcError(-32001, "Complete the daemon handshake first.");
122
+ switch (value.method) {
123
+ case "daemon.status":
124
+ result = snapshot();
125
+ break;
126
+ case "daemon.stop":
127
+ result = { stopped: true };
128
+ setImmediate(close);
129
+ break;
130
+ case "devices.status":
131
+ result = await deviceStatus.read();
132
+ break;
133
+ case "devices.list":
134
+ result = await connector.list();
135
+ break;
136
+ case "devices.run":
137
+ if (typeof params.deviceId !== "string" || typeof params.action !== "string")
138
+ throw new RpcError(-32602, "A device ID and action are required.");
139
+ result = await connector.run(params.deviceId, params.action, params.args ?? {});
140
+ break;
141
+ case "events.subscribe":
142
+ case "events.unsubscribe":
143
+ if (params.topic !== "connection")
144
+ throw new RpcError(-32602, "Unknown event topic.");
145
+ peer.subscribed = value.method === "events.subscribe";
146
+ if (peer.subscribed)
147
+ notify(peer);
148
+ result = { subscribed: peer.subscribed };
149
+ break;
150
+ default:
151
+ throw new RpcError(-32601, "Method not found");
152
+ }
153
+ }
154
+ return hasId ? { jsonrpc: "2.0", id, result } : undefined;
155
+ }
156
+ catch (cause) {
157
+ if (!hasId)
158
+ return;
159
+ return cause instanceof RpcError
160
+ ? error(id, cause.code, cause.message)
161
+ : error(id, -32000, "The device service is unavailable.");
162
+ }
163
+ finally {
164
+ if (hasId && id !== null)
165
+ peer.pending.delete(id);
166
+ }
167
+ }
168
+ sockets.on("connection", (socket) => {
169
+ const peer = { socket, ready: false, subscribed: false, alive: true, pending: new Set() };
170
+ peers.add(peer);
171
+ socket.on("error", () => socket.terminate());
172
+ socket.on("close", () => peers.delete(peer));
173
+ socket.on("pong", () => {
174
+ peer.alive = true;
175
+ });
176
+ socket.on("message", (data, binary) => {
177
+ let request;
178
+ try {
179
+ request = JSON.parse(data.toString());
180
+ }
181
+ catch {
182
+ send(peer, error(null, -32700, "Parse error"));
183
+ return;
184
+ }
185
+ if (binary) {
186
+ send(peer, error(null, -32600, "Invalid Request"));
187
+ return;
188
+ }
189
+ void (async () => {
190
+ if (Array.isArray(request) && request.length > 0) {
191
+ const responses = (await Promise.all(request.map((item) => dispatch(peer, item)))).filter((value) => value !== undefined);
192
+ if (responses.length)
193
+ send(peer, responses);
194
+ }
195
+ else {
196
+ const response = await dispatch(peer, request);
197
+ if (response !== undefined)
198
+ send(peer, response);
199
+ }
200
+ })().catch(() => socket.terminate());
201
+ });
202
+ });
203
+ await new Promise((resolve, reject) => {
204
+ server.once("error", reject);
205
+ server.listen(state.port, "127.0.0.1", () => {
206
+ server.off("error", reject);
207
+ resolve();
208
+ });
209
+ });
210
+ const heartbeat = setInterval(() => {
211
+ for (const peer of peers) {
212
+ if (!peer.alive) {
213
+ peer.socket.terminate();
214
+ continue;
215
+ }
216
+ peer.alive = false;
217
+ if (peer.socket.readyState === WebSocket.OPEN)
218
+ peer.socket.ping();
219
+ }
220
+ }, 30_000);
221
+ heartbeat.unref();
222
+ const finished = new Promise((resolve) => server.once("close", resolve));
223
+ signal.addEventListener("abort", close, { once: true });
224
+ try {
225
+ await writePrivateJson(daemonStatePath(config), state);
226
+ if (signal.aborted)
227
+ close();
228
+ await finished;
229
+ }
230
+ finally {
231
+ close();
232
+ clearInterval(heartbeat);
233
+ signal.removeEventListener("abort", close);
234
+ }
235
+ }
@@ -0,0 +1,23 @@
1
+ import type { ConnectionStatus } from "./client.js";
2
+ import type { DeviceConnector } from "./connector.js";
3
+ export interface DeviceStatus {
4
+ id: string;
5
+ name: string;
6
+ platform: string | null;
7
+ status: "connected" | "not_connected" | "unknown" | "revoked";
8
+ }
9
+ export interface DevicesStatus {
10
+ connection: ConnectionStatus;
11
+ checkedAt: string;
12
+ devices: DeviceStatus[];
13
+ }
14
+ export declare class DeviceStatusReader {
15
+ private connector;
16
+ private cached?;
17
+ private pending?;
18
+ private generation;
19
+ constructor(connector: Pick<DeviceConnector, "list" | "run" | "getStatus">);
20
+ invalidate(): void;
21
+ read(): Promise<DevicesStatus>;
22
+ private check;
23
+ }
@@ -0,0 +1,68 @@
1
+ import { isRecord } from "./actions.js";
2
+ export class DeviceStatusReader {
3
+ connector;
4
+ cached;
5
+ pending;
6
+ generation = 0;
7
+ constructor(connector) {
8
+ this.connector = connector;
9
+ }
10
+ invalidate() {
11
+ this.generation++;
12
+ this.cached = undefined;
13
+ }
14
+ async read() {
15
+ if (this.cached && Date.now() - Date.parse(this.cached.checkedAt) < 10_000)
16
+ return this.cached;
17
+ if (this.pending)
18
+ return this.pending;
19
+ const generation = this.generation;
20
+ this.pending = this.check()
21
+ .then((snapshot) => {
22
+ if (generation === this.generation)
23
+ this.cached = snapshot;
24
+ return snapshot;
25
+ })
26
+ .finally(() => {
27
+ this.pending = undefined;
28
+ });
29
+ return this.pending;
30
+ }
31
+ async check() {
32
+ const list = await this.connector.list();
33
+ if (!isRecord(list) || !Array.isArray(list.items))
34
+ throw new Error("Invalid device list");
35
+ const devices = list.items.map((item) => {
36
+ if (!isRecord(item) || typeof item.id !== "string")
37
+ throw new Error("Invalid device record");
38
+ return {
39
+ id: item.id,
40
+ name: typeof item.device_name === "string" ? item.device_name : item.id,
41
+ platform: typeof item.platform === "string" ? item.platform : null,
42
+ status: item.revoked_at ? "revoked" : "unknown",
43
+ };
44
+ });
45
+ const deadline = Date.now() + 15_000;
46
+ let next = 0;
47
+ await Promise.all(Array.from({ length: Math.min(4, devices.length) }, async () => {
48
+ while (next < devices.length && Date.now() < deadline) {
49
+ const device = devices[next++];
50
+ if (device.status === "revoked")
51
+ continue;
52
+ const response = await this.connector.run(device.id, "system.info", {}, 3000);
53
+ device.status = response.ok
54
+ ? "connected"
55
+ : response.error.code === "target_unavailable"
56
+ ? "not_connected"
57
+ : "unknown";
58
+ }
59
+ }));
60
+ const connection = this.connector.getStatus();
61
+ if (connection !== "online") {
62
+ for (const device of devices)
63
+ if (device.status !== "revoked")
64
+ device.status = "unknown";
65
+ }
66
+ return { connection, checkedAt: new Date().toISOString(), devices };
67
+ }
68
+ }
@@ -0,0 +1,5 @@
1
+ import type { InboundEnvelope, OutboundEnvelope } from "./protocol.js";
2
+ export declare function createExecutor(name: string, send: (message: OutboundEnvelope) => boolean, secrets?: string[]): {
3
+ receive(message: InboundEnvelope): Promise<void>;
4
+ disconnect(): void;
5
+ };
@@ -0,0 +1,105 @@
1
+ import { spawn } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { actionError, isRecord } from "./actions.js";
4
+ export function createExecutor(name, send, secrets = []) {
5
+ const children = new Set();
6
+ let generation = 0;
7
+ const handlers = {
8
+ "system.info": async () => ({
9
+ type: "response",
10
+ ok: true,
11
+ result: {
12
+ name,
13
+ platform: platform() === "darwin" ? "macos" : platform() === "win32" ? "windows" : platform(),
14
+ actions: ["system.info", "exec"],
15
+ },
16
+ }),
17
+ exec: async (input) => {
18
+ if (!isRecord(input) ||
19
+ typeof input.command !== "string" ||
20
+ !input.command ||
21
+ input.command.includes("\0") ||
22
+ (input.args !== undefined &&
23
+ (!Array.isArray(input.args) ||
24
+ !input.args.every((arg) => typeof arg === "string" && !arg.includes("\0")))) ||
25
+ (input.cwd !== undefined && (typeof input.cwd !== "string" || input.cwd.includes("\0"))))
26
+ return actionError("invalid_args", "exec requires command, optional string args[], and optional cwd.");
27
+ if (children.size >= 8)
28
+ return actionError("busy", "The computer is already running eight programs.");
29
+ const command = input.command;
30
+ const args = (input.args ?? []);
31
+ const cwd = input.cwd;
32
+ return new Promise((resolve) => {
33
+ const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(?:ROME_.*(?:TOKEN|SECRET|CREDENTIAL)|NODE_OPTIONS)$/i.test(key)));
34
+ let child;
35
+ try {
36
+ child = spawn(command, args, {
37
+ cwd,
38
+ env: environment,
39
+ shell: false,
40
+ windowsHide: true,
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ });
43
+ }
44
+ catch {
45
+ resolve(actionError("exec_failed", "Could not start the program."));
46
+ return;
47
+ }
48
+ children.add(child);
49
+ const output = { stdout: [], stderr: [] };
50
+ for (const stream of ["stdout", "stderr"]) {
51
+ child[stream]?.on("data", (chunk) => {
52
+ output[stream].push(chunk);
53
+ });
54
+ }
55
+ child.once("error", () => {
56
+ children.delete(child);
57
+ resolve(actionError("exec_failed", "Could not start the program. Check the executable and working directory."));
58
+ });
59
+ child.once("close", (exitCode, signal) => {
60
+ children.delete(child);
61
+ const redact = (chunks) => secrets.reduce((text, secret) => text.replaceAll(secret, "[redacted]"), Buffer.concat(chunks).toString("utf8"));
62
+ resolve({
63
+ type: "response",
64
+ ok: true,
65
+ result: {
66
+ exitCode,
67
+ signal,
68
+ stdout: redact(output.stdout),
69
+ stderr: redact(output.stderr),
70
+ truncated: { stdout: false, stderr: false },
71
+ },
72
+ });
73
+ });
74
+ });
75
+ },
76
+ };
77
+ return {
78
+ async receive(message) {
79
+ const request = message.payload;
80
+ if (!isRecord(request) || request.type !== "request" || typeof request.action !== "string")
81
+ return;
82
+ const current = generation;
83
+ const handler = Object.hasOwn(handlers, request.action)
84
+ ? handlers[request.action]
85
+ : undefined;
86
+ const payload = handler
87
+ ? await handler(request.args).catch(() => actionError("exec_failed", "The action failed."))
88
+ : actionError("unsupported_action", "The computer does not support this action.");
89
+ const reply = { id: message.id, to: message.from, payload };
90
+ if (current === generation)
91
+ send(reply);
92
+ },
93
+ disconnect() {
94
+ generation++;
95
+ for (const child of children) {
96
+ child.kill("SIGTERM");
97
+ const timer = setTimeout(() => {
98
+ if (child.exitCode === null && child.signalCode === null)
99
+ child.kill("SIGKILL");
100
+ }, 1000);
101
+ timer.unref();
102
+ }
103
+ },
104
+ };
105
+ }
@@ -0,0 +1 @@
1
+ export declare function parseGatewayUrl(value: string): URL;
@@ -0,0 +1,11 @@
1
+ export function parseGatewayUrl(value) {
2
+ const url = new URL(value);
3
+ if (url.username ||
4
+ url.password ||
5
+ url.search ||
6
+ url.hash ||
7
+ (url.protocol !== "wss:" &&
8
+ !(url.protocol === "ws:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
9
+ throw new Error("Use a credential-free WSS URL. WS is allowed only on loopback.");
10
+ return url;
11
+ }
package/dist/host.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { connectHost, defaultDeviceName, type HostCredential, type HostEvent, type HostOptions, } from "./connect.js";
2
+ export { loginDevice, type DeviceSession } from "./login.js";
3
+ export { createFileCredentialStore, type CredentialStore } from "./storage.js";
package/dist/host.js ADDED
@@ -0,0 +1,3 @@
1
+ export { connectHost, defaultDeviceName, } from "./connect.js";
2
+ export { loginDevice } from "./login.js";
3
+ export { createFileCredentialStore } from "./storage.js";
@@ -0,0 +1,4 @@
1
+ export { connectGateway, type GatewayClientOptions, type ConnectionStatus, type ClientSocket, } from "./client.js";
2
+ export { MAX_MESSAGE_BYTES, byteLength, validId, type GatewayMessage, type InboundEnvelope, type OutboundEnvelope, } from "./protocol.js";
3
+ export { CloudError, cloudOrigin, cloudRequest, gatewayConfig } from "./cloud.js";
4
+ export { actionError, isRecord, parseResponse, type ActionResponse, type ActionRequest, } from "./actions.js";