@rome-os/node 0.1.0 → 0.1.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/README.md CHANGED
@@ -8,8 +8,9 @@ Build an installation archive from this repository:
8
8
 
9
9
  ```sh
10
10
  pnpm --filter @rome-os/node build
11
+ pnpm --filter @rome-os/node-core pack --pack-destination /tmp
11
12
  pnpm --filter @rome-os/node pack --pack-destination /tmp
12
- npm install -g /tmp/rome-os-node-0.1.0.tgz
13
+ npm install -g /tmp/rome-os-node-core-0.1.0.tgz /tmp/rome-os-node-0.1.0.tgz
13
14
  ```
14
15
 
15
16
  On the target computer:
@@ -22,7 +23,17 @@ Open the printed authorization URL in a browser on that computer and approve it.
22
23
  The browser callback uses a temporary loopback listener. A remote browser needs
23
24
  a tunnel to that listener. Ctrl+C disconnects and preserves the credential.
24
25
 
25
- On the caller, configure a previously issued communication token through stdin:
26
+ Rome instances prepare caller authorization automatically at startup and after
27
+ Cloud enrollment. In a server environment that supplies `ROME_INSTANCE_TOKEN`, run:
28
+
29
+ ```sh
30
+ rome-node auth --server --cloud https://romeos.cc
31
+ ```
32
+
33
+ This exchanges the Instance Token with Cloud and stores only the communication
34
+ token. Existing caller credentials are reused. The CLI does not read Rome's database.
35
+
36
+ For manual caller configuration, pass an issued communication token through stdin:
26
37
 
27
38
  ```sh
28
39
  rome-node auth --cloud https://romeos.cc < /path/to/private-token-file
@@ -49,10 +60,21 @@ Gateway connection. `rome-node daemon status` reports its PID, and
49
60
  daemon before changing credentials. `rome-node daemon serve` runs in the
50
61
  foreground for diagnosis. Set `ROME_NODE_CONFIG_DIR` to isolate configurations.
51
62
 
63
+ `rome-node watch` subscribes to caller connection changes and prints one JSON
64
+ event per line. It starts the daemon if needed and begins with a snapshot.
65
+ After a connection loss it waits for the daemon and subscribes again; it does
66
+ not restart a stopped daemon. Ctrl+C closes only the watcher. These events
67
+ report the caller's Gateway connection, not remote device presence.
68
+
52
69
  Commands print JSON to stdout and diagnostics to stderr. Errors and nonzero
53
- remote exit codes produce a nonzero local exit status. An authorized device is
70
+ remote exit codes produce a nonzero local exit status. Output is collected in memory
71
+ and returned in full after program exit. Action message limits follow the WebSocket library and infrastructure.
72
+ The local daemon disconnects event observers that exceed its send-buffer limit. A lost response reports
73
+ `unknown_outcome`. Never automatically retry an unknown outcome.
74
+ An authorized device is
54
75
  not necessarily online. Use `describe` to query its current platform and actions.
55
76
 
56
- The package exports the Gateway client, socket adapter, and action response
57
- contract for other clients. See [the device architecture and operating guide](../../docs/rome-node.md)
77
+ The CLI uses [`@rome-os/node-core`](../rome-node-core/README.md) for authorization,
78
+ its shared caller daemon, and the computer host. The package also re-exports
79
+ the Gateway client, socket adapter, and action response contract for compatibility. See [the device architecture and operating guide](../../docs/rome-node.md)
58
80
  for credential boundaries, file operations, output limits, and disconnection behavior.
package/dist/entry.js CHANGED
@@ -1,12 +1,26 @@
1
1
  import { parseArgs } from "node:util";
2
- import { connectComputer, defaultDeviceName } from "./connect.js";
3
- import { isRecord, parseResponse } from "./actions.js";
4
- import { callerCredentialPath } from "./local.js";
5
- import { writePrivateJson } from "./storage.js";
6
- import { cloudOrigin, gatewayConfig } from "./cloud.js";
7
- import { callDaemon, daemonStatus, serveDaemon, startDaemon, stopDaemon } from "./daemon.js";
8
- import { validId } from "./protocol.js";
2
+ import { join } from "node:path";
3
+ import { isRecord, cloudOrigin, validId } from "@rome-os/node-core";
4
+ import { authorizeServer, configureCaller } from "@rome-os/node-core/auth";
5
+ import { createNodeClient, CallerConfigurationError, DeviceActionError, nodeConfigFromEnvironment, readOptionalCallerCredential, daemonStatus, startDaemon, stopDaemon, } from "@rome-os/node-core/client";
9
6
  import { commandHelp } from "./help.js";
7
+ async function withShutdown(run) {
8
+ const controller = new AbortController();
9
+ const stop = () => controller.abort();
10
+ process.once("SIGINT", stop);
11
+ process.once("SIGTERM", stop);
12
+ try {
13
+ await run(controller.signal);
14
+ }
15
+ catch (error) {
16
+ if (!controller.signal.aborted)
17
+ throw error;
18
+ }
19
+ finally {
20
+ process.removeListener("SIGINT", stop);
21
+ process.removeListener("SIGTERM", stop);
22
+ }
23
+ }
10
24
  async function main() {
11
25
  const { values, positionals } = parseArgs({
12
26
  allowPositionals: true,
@@ -16,6 +30,7 @@ async function main() {
16
30
  cloud: { type: "string" },
17
31
  name: { type: "string" },
18
32
  args: { type: "string" },
33
+ server: { type: "boolean" },
19
34
  },
20
35
  });
21
36
  if (values.help || positionals.length === 0 || positionals[0] === "help") {
@@ -23,10 +38,26 @@ async function main() {
23
38
  process.stdout.write(commandHelp(explicitTopic ? positionals.slice(1) : positionals, explicitTopic));
24
39
  return;
25
40
  }
41
+ const config = nodeConfigFromEnvironment(process.env);
42
+ if (values.server && !(positionals[0] === "auth" && positionals.length === 1))
43
+ throw new Error("--server is only supported by rome-node auth.");
44
+ if (positionals[0] === "auth" && positionals[1] === "status" && positionals.length === 2) {
45
+ const credential = await readOptionalCallerCredential(config);
46
+ process.stdout.write(`${JSON.stringify(credential ? { configured: true, cloudUrl: credential.cloudUrl } : { configured: false })}\n`);
47
+ return;
48
+ }
26
49
  if (positionals[0] === "auth" && positionals.length === 1) {
50
+ const cloudUrl = cloudOrigin(values.cloud ?? "https://romeos.cc");
51
+ if (values.server) {
52
+ await authorizeServer(cloudUrl, process.env.ROME_INSTANCE_TOKEN, config, () => {
53
+ process.stderr.write("Rome Cloud is temporarily unavailable. Retrying server authorization.\n");
54
+ });
55
+ process.stdout.write(`${JSON.stringify({ configured: true })}\n`);
56
+ return;
57
+ }
27
58
  if (process.stdin.isTTY)
28
59
  throw new Error("Pass the communication token through stdin, not command arguments.");
29
- if (await daemonStatus())
60
+ if (await daemonStatus(config))
30
61
  throw new Error("Stop the CLI daemon before changing credentials: rome-node daemon stop");
31
62
  const chunks = [];
32
63
  let size = 0;
@@ -38,11 +69,7 @@ async function main() {
38
69
  chunks.push(buffer);
39
70
  }
40
71
  const token = Buffer.concat(chunks).toString("utf8").trim();
41
- if (!/^romedev_[A-Za-z0-9_-]{43}$/.test(token))
42
- throw new Error("Invalid communication token.");
43
- const cloudUrl = cloudOrigin(values.cloud ?? "https://romeos.cc");
44
- await gatewayConfig(cloudUrl, token);
45
- await writePrivateJson(callerCredentialPath(), { cloudUrl, token });
72
+ await configureCaller(cloudUrl, token, config);
46
73
  process.stdout.write(`${JSON.stringify({ configured: true })}\n`);
47
74
  return;
48
75
  }
@@ -51,36 +78,68 @@ async function main() {
51
78
  throw new Error("Invalid daemon command.");
52
79
  const operation = positionals[1] ?? "start";
53
80
  if (operation === "serve") {
54
- await serveDaemon();
81
+ const { serveDaemon } = await import("@rome-os/node-core/daemon");
82
+ await withShutdown((signal) => serveDaemon(config, signal));
55
83
  return;
56
84
  }
57
85
  if (operation === "stop") {
58
- await stopDaemon();
86
+ await stopDaemon(config);
59
87
  process.stdout.write(`${JSON.stringify({ running: false })}\n`);
60
88
  return;
61
89
  }
62
90
  if (operation !== "start" && operation !== "status")
63
91
  throw new Error("Unknown daemon command.");
64
- const state = operation === "start" ? await startDaemon() : await daemonStatus();
92
+ const state = operation === "start" ? await startDaemon(config) : await daemonStatus(config);
65
93
  process.stdout.write(`${JSON.stringify(state ? { running: true, pid: state.pid } : { running: false })}\n`);
66
94
  return;
67
95
  }
96
+ if (positionals[0] === "watch" && positionals.length === 1) {
97
+ await withShutdown(async (signal) => {
98
+ const client = createNodeClient(config);
99
+ let finish;
100
+ const stopped = new Promise((resolve) => {
101
+ finish = resolve;
102
+ });
103
+ const stop = () => {
104
+ client.disconnect();
105
+ finish();
106
+ };
107
+ signal.addEventListener("abort", stop, { once: true });
108
+ try {
109
+ if (signal.aborted)
110
+ return;
111
+ await client.subscribe("connection", (event) => process.stdout.write(`${JSON.stringify(event)}\n`));
112
+ await stopped;
113
+ }
114
+ finally {
115
+ signal.removeEventListener("abort", stop);
116
+ client.disconnect();
117
+ }
118
+ });
119
+ return;
120
+ }
68
121
  if (positionals[0] === "connect" && positionals.length === 1) {
69
- const controller = new AbortController();
70
- const stop = () => controller.abort();
71
- process.once("SIGINT", stop);
72
- process.once("SIGTERM", stop);
73
- try {
74
- await connectComputer(values.cloud ?? "https://romeos.cc", values.name ?? defaultDeviceName(), controller.signal);
75
- }
76
- catch (error) {
77
- if (!controller.signal.aborted)
78
- throw error;
79
- }
80
- finally {
81
- process.removeListener("SIGINT", stop);
82
- process.removeListener("SIGTERM", stop);
83
- }
122
+ const { connectHost, defaultDeviceName, loginDevice, createFileCredentialStore } = await import("@rome-os/node-core/host");
123
+ const name = values.name ?? defaultDeviceName();
124
+ await withShutdown((signal) => connectHost({
125
+ cloudUrl: values.cloud ?? "https://romeos.cc",
126
+ name,
127
+ signal,
128
+ credentials: createFileCredentialStore(join(config.directory, "credential.json")),
129
+ authorize: (origin, deviceName, abort) => loginDevice(origin, deviceName, abort, (url) => {
130
+ process.stderr.write(`Open this URL in your browser to authorize ${deviceName}:\n${url}\n`);
131
+ }),
132
+ onEvent: (event) => {
133
+ if (event.type === "authorization_required")
134
+ process.stderr.write("While connect is running, Rome instances in your account can execute programs and read or change files as your OS user.\n");
135
+ else if (event.type === "retrying")
136
+ process.stderr.write("Rome Cloud is unavailable; keeping credentials and retrying.\n");
137
+ else if (event.type === "device")
138
+ process.stderr.write(`Device: ${event.name}\nDevice ID: ${event.deviceId}\n`);
139
+ else
140
+ process.stderr.write(`Connection: ${event.status}\n`);
141
+ },
142
+ }));
84
143
  return;
85
144
  }
86
145
  if (positionals[0] !== "device")
@@ -102,26 +161,42 @@ async function main() {
102
161
  throw new Error("--args must contain valid JSON.");
103
162
  }
104
163
  }
105
- const response = await callDaemon(listing ? "/devices" : "/run", listing
106
- ? undefined
107
- : { deviceId, action: command === "describe" ? "system.info" : action, args });
108
- const body = await response.json();
109
- if (listing && response.ok && isRecord(body) && Array.isArray(body.items)) {
110
- process.stdout.write(`${JSON.stringify({ items: body.items })}\n`);
111
- return;
164
+ const client = createNodeClient(config);
165
+ try {
166
+ if (listing) {
167
+ process.stdout.write(`${JSON.stringify(await client.listDevices())}\n`);
168
+ return;
169
+ }
170
+ const result = command === "describe"
171
+ ? await client.describe(deviceId)
172
+ : await client.run(deviceId, action, args);
173
+ process.stdout.write(`${JSON.stringify(result)}\n`);
174
+ if (!result.ok ||
175
+ (isRecord(result.result) &&
176
+ Object.hasOwn(result.result, "exitCode") &&
177
+ result.result.exitCode !== 0))
178
+ process.exitCode = 1;
179
+ }
180
+ finally {
181
+ client.disconnect();
112
182
  }
113
- const result = parseResponse(body);
114
- if (!result)
115
- throw new Error("Invalid response from the CLI daemon.");
116
- process.stdout.write(`${JSON.stringify(result)}\n`);
117
- if (!result.ok ||
118
- !response.ok ||
119
- (isRecord(result.result) &&
120
- Object.hasOwn(result.result, "exitCode") &&
121
- result.result.exitCode !== 0))
122
- process.exitCode = 1;
123
183
  }
124
184
  main().catch((error) => {
185
+ if (error instanceof DeviceActionError) {
186
+ process.stdout.write(`${JSON.stringify(error.response)}\n`);
187
+ process.exitCode = 1;
188
+ return;
189
+ }
190
+ if (error instanceof CallerConfigurationError) {
191
+ const messages = {
192
+ not_configured: "Configure a communication token with rome-node auth --cloud <origin> using stdin first.",
193
+ daemon_running: "Stop the CLI daemon before changing credentials: rome-node daemon stop",
194
+ instance_token_required: "Server authorization requires ROME_INSTANCE_TOKEN in the environment.",
195
+ };
196
+ process.stderr.write(`${messages[error.code]}\n`);
197
+ process.exitCode = 1;
198
+ return;
199
+ }
125
200
  process.stderr.write(`${error instanceof Error ? error.message : "Rome Node failed."}\n`);
126
201
  process.exitCode = 1;
127
202
  });
package/dist/help.js CHANGED
@@ -6,10 +6,12 @@ Usage:
6
6
 
7
7
  Commands:
8
8
  connect Keep this computer online as a remote executor.
9
- auth Configure the caller's communication token through stdin.
9
+ auth Configure caller authorization through stdin or server mode.
10
+ auth status Report local caller configuration without revealing the token.
10
11
  device List authorized devices (they may be offline).
11
12
  device describe Query a device's platform and supported actions.
12
13
  device run Execute an action on a device.
14
+ watch Stream caller connection snapshots and changes as JSON lines.
13
15
  daemon Manage the caller's background connection.
14
16
 
15
17
  Agent workflow:
@@ -23,6 +25,19 @@ Use -h or --help on any command. No arguments also prints this overview.
23
25
  Set ROME_NODE_CONFIG_DIR to use a separate configuration directory.
24
26
  `;
25
27
  const commands = {
28
+ watch: `Usage: rome-node watch
29
+
30
+ Subscribe to caller connection state as newline-delimited JSON.
31
+ Starts the daemon if needed, but does not open its Gateway connection.
32
+ Each connected event includes {transport:"connected",daemon:{pid,protocolVersion,connection}}.
33
+ Local disconnection emits {transport:"disconnected",daemon:null,reason:"connection_lost"}.
34
+ The daemon connection and its Gateway connection are separate states.
35
+
36
+ Reconnection restores the subscription and emits a fresh snapshot, not missed history.
37
+ A watcher does not restart an explicitly stopped daemon. Another command can start it.
38
+ Ctrl+C exits the watcher without stopping the daemon or remote operations.
39
+ These events do not indicate whether individual remote devices are online.
40
+ `,
26
41
  connect: `Usage: rome-node connect [--cloud <origin>] [--name <name>]
27
42
 
28
43
  Run this on the computer that will execute programs.
@@ -37,22 +52,39 @@ Options:
37
52
  Example:
38
53
  rome-node connect --name "My computer"
39
54
  `,
40
- auth: `Usage: rome-node auth [--cloud <origin>] < token-file
55
+ auth: `Usage:
56
+ rome-node auth [--cloud <origin>] < token-file
57
+ rome-node auth --server [--cloud <origin>]
41
58
 
42
59
  Read an issued communication token (romedev_...) from stdin, validate it with
43
- Cloud, and save it privately for device commands. An Instance Token must first
44
- be exchanged through Cloud. This command does not perform that exchange.
60
+ Cloud, and save it privately for device commands.
45
61
  Stop a running caller daemon before changing credentials.
46
62
 
63
+ With --server, reuse existing caller credentials for this Cloud origin. If missing,
64
+ read ROME_INSTANCE_TOKEN from the environment, exchange it with Cloud, and save
65
+ only the communication token. This does not start a daemon or read Rome's database.
66
+ Transient Cloud failures get up to three attempts with delays of one and five seconds.
67
+ An issued token is reused across validation retries. Existing credentials for
68
+ another Cloud origin are preserved and require manual configuration.
69
+
47
70
  Options:
48
71
  --cloud <origin> Cloud origin (default: https://romeos.cc).
72
+ --server Authorize using ROME_INSTANCE_TOKEN from the environment.
49
73
 
50
74
  Example:
51
75
  rome-node daemon stop
52
76
  rome-node auth --cloud https://romeos.cc < /path/to/private-token-file
77
+ rome-node auth --server --cloud https://romeos.cc
53
78
 
54
79
  Output: {"configured":true}
55
- Pass tokens through stdin. Never put them in command arguments or print them.
80
+ Pass communication tokens through stdin. Never put tokens in command arguments or print them.
81
+ `,
82
+ "auth status": `Usage: rome-node auth status
83
+
84
+ Report local caller configuration without contacting Cloud or starting the daemon.
85
+ Output: {"configured":true,"cloudUrl":"https://romeos.cc"} or {"configured":false}.
86
+ This checks the local credential format, not its validity at Cloud.
87
+ The token is never printed.
56
88
  `,
57
89
  device: `Usage:
58
90
  rome-node device
@@ -112,8 +144,9 @@ Output:
112
144
  Check exec's exitCode even when ok is true. The local exit status is 1 for
113
145
  action failures, nonzero remote exit codes, or signal termination, otherwise 0.
114
146
  Local setup and argument errors print diagnostics to stderr and exit 1.
115
- Each output stream retains at most 48 KiB, with further truncation to fit a frame.
116
- Check truncated before treating output or file contents as complete.
147
+ Output is collected in memory and returned in full after program exit.
148
+ The CLI does not truncate output or impose message or send-buffer size limits.
149
+ Transport limits still apply. Connection loss returns unknown_outcome.
117
150
 
118
151
  The caller waits up to 60 seconds for a response. unknown_outcome means execution
119
152
  may have started or completed. A local timeout does not cancel the remote program.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,2 @@
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";
5
- export { createNodeSocket } from "./socket.js";
1
+ export * from "@rome-os/node-core";
2
+ export { createNodeSocket } from "@rome-os/node-core/socket";
package/dist/index.js CHANGED
@@ -1,5 +1,2 @@
1
- export { connectGateway, } from "./client.js";
2
- export { MAX_MESSAGE_BYTES, byteLength, validId, } from "./protocol.js";
3
- export { CloudError, cloudOrigin, cloudRequest, gatewayConfig } from "./cloud.js";
4
- export { actionError, isRecord, parseResponse, } from "./actions.js";
5
- export { createNodeSocket } from "./socket.js";
1
+ export * from "@rome-os/node-core";
2
+ export { createNodeSocket } from "@rome-os/node-core/socket";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rome-os/node",
3
- "version": "0.1.0",
4
- "description": "Rome device protocol, Gateway client, and remote computer CLI",
3
+ "version": "0.1.1",
4
+ "description": "Standalone CLI for Rome remote devices",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
@@ -22,21 +22,22 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "ws": "^8.18.0"
25
+ "@rome-os/node-core": "^0.1.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@rstest/core": "^0.11.11",
29
29
  "@types/node": "25.2.3",
30
30
  "@types/ws": "^8.18.0",
31
- "typescript": "^5.9.3"
31
+ "typescript": "^5.9.3",
32
+ "ws": "^8.18.0"
32
33
  },
33
34
  "repository": {
34
35
  "type": "git",
35
36
  "url": "git+https://github.com/rome-os/rome.git",
36
- "directory": "packages/rome-node"
37
+ "directory": "packages/rome-node-cli"
37
38
  },
38
39
  "scripts": {
39
- "build": "tsc -p tsconfig.build.json",
40
+ "build": "pnpm --filter @rome-os/node-core build && node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && tsc -p tsconfig.build.json",
40
41
  "typecheck": "tsc --noEmit",
41
42
  "test": "pnpm build && ../../scripts/test-env.sh rstest"
42
43
  }
package/dist/actions.d.ts DELETED
@@ -1,20 +0,0 @@
1
- export interface ActionRequest {
2
- type: "request";
3
- action: string;
4
- args: unknown;
5
- }
6
- export type ActionResponse = {
7
- type: "response";
8
- ok: true;
9
- result: unknown;
10
- } | {
11
- type: "response";
12
- ok: false;
13
- error: {
14
- code: string;
15
- message: string;
16
- };
17
- };
18
- export declare function actionError(code: string, message: string): ActionResponse;
19
- export declare function isRecord(value: unknown): value is Record<string, unknown>;
20
- export declare function parseResponse(value: unknown): ActionResponse | null;
package/dist/actions.js DELETED
@@ -1,18 +0,0 @@
1
- export function actionError(code, message) {
2
- return { type: "response", ok: false, error: { code, message } };
3
- }
4
- export function isRecord(value) {
5
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
- }
7
- export function parseResponse(value) {
8
- if (!isRecord(value) || value.type !== "response")
9
- return null;
10
- if (value.ok === true && Object.hasOwn(value, "result"))
11
- return { type: "response", ok: true, result: value.result };
12
- if (value.ok === false &&
13
- isRecord(value.error) &&
14
- typeof value.error.code === "string" &&
15
- typeof value.error.message === "string")
16
- return actionError(value.error.code, value.error.message);
17
- return null;
18
- }
package/dist/client.d.ts DELETED
@@ -1,29 +0,0 @@
1
- import { type GatewayMessage, type OutboundEnvelope } from "./protocol.js";
2
- export interface ClientSocket {
3
- readyState: number;
4
- bufferedAmount: number;
5
- send(data: string): void;
6
- close(code?: number, reason?: string): void;
7
- addEventListener(type: "open", listener: () => void): void;
8
- addEventListener(type: "message", listener: (event: {
9
- data: unknown;
10
- }) => void): void;
11
- addEventListener(type: "close", listener: (event: {
12
- code: number;
13
- }) => void): void;
14
- addEventListener(type: "error", listener: () => void): void;
15
- }
16
- export type ConnectionStatus = "connecting" | "online" | "retrying" | "revoked" | "superseded" | "stopped";
17
- export interface GatewayClientOptions {
18
- gatewayUrl: string;
19
- deviceToken: string;
20
- createSocket(url: string, authorization: string): ClientSocket;
21
- onMessage(message: GatewayMessage): void;
22
- onStatus?(status: ConnectionStatus): void;
23
- random?(): number;
24
- beforeConnect?(): Promise<string | null>;
25
- }
26
- export declare function connectGateway(options: GatewayClientOptions): {
27
- send(envelope: OutboundEnvelope): boolean;
28
- stop(): void;
29
- };
package/dist/client.js DELETED
@@ -1,143 +0,0 @@
1
- import { byteLength, CLOSE, gatewayMessage, MAX_CLIENT_BUFFER_BYTES, MAX_MESSAGE_BYTES, outboundEnvelope, } from "./protocol.js";
2
- export function connectGateway(options) {
3
- let url = new URL(options.gatewayUrl);
4
- if (url.protocol !== "wss:" || url.username || url.password || url.search || url.hash)
5
- throw new Error("A credential-free WSS URL is required");
6
- if (!/^(?:romedev_|romemob_)[A-Za-z0-9_-]{43}$/.test(options.deviceToken))
7
- throw new Error("Invalid device token format");
8
- let stopped = false;
9
- let socket = null;
10
- let attempt = 0;
11
- let timer;
12
- let connectingTimer;
13
- let stableTimer;
14
- const status = (value) => options.onStatus?.(value);
15
- const schedule = () => {
16
- if (stopped || timer)
17
- return;
18
- status("retrying");
19
- const delay = Math.min(60_000, 1000 * 2 ** Math.min(attempt++, 6));
20
- timer = setTimeout(() => {
21
- timer = undefined;
22
- void open();
23
- }, delay * (0.75 + (options.random ?? Math.random)() * 0.5));
24
- };
25
- const open = async () => {
26
- if (stopped)
27
- return;
28
- status("connecting");
29
- let current;
30
- try {
31
- if (options.beforeConnect) {
32
- const next = await options.beforeConnect();
33
- if (stopped)
34
- return;
35
- if (next === null) {
36
- stopped = true;
37
- status("revoked");
38
- return;
39
- }
40
- url = new URL(next);
41
- if (url.protocol !== "wss:" || url.username || url.password || url.search || url.hash)
42
- throw new Error("A credential-free WSS URL is required");
43
- }
44
- current = options.createSocket(url.toString(), `Bearer ${options.deviceToken}`);
45
- }
46
- catch {
47
- schedule();
48
- return;
49
- }
50
- socket = current;
51
- let finished = false;
52
- const finish = (code) => {
53
- if (finished || socket !== current)
54
- return;
55
- finished = true;
56
- clearTimeout(connectingTimer);
57
- clearTimeout(stableTimer);
58
- socket = null;
59
- if (code === CLOSE.revoked || code === CLOSE.superseded) {
60
- stopped = true;
61
- status(code === CLOSE.revoked ? "revoked" : "superseded");
62
- }
63
- else
64
- schedule();
65
- };
66
- connectingTimer = setTimeout(() => {
67
- finish();
68
- current.close();
69
- }, 10_000);
70
- current.addEventListener("open", () => {
71
- if (stopped || socket !== current) {
72
- current.close();
73
- return;
74
- }
75
- clearTimeout(connectingTimer);
76
- stableTimer = setTimeout(() => {
77
- attempt = 0;
78
- }, 30_000);
79
- status("online");
80
- });
81
- current.addEventListener("message", ({ data }) => {
82
- if (stopped ||
83
- socket !== current ||
84
- typeof data !== "string" ||
85
- byteLength(data) > MAX_MESSAGE_BYTES)
86
- return;
87
- let message;
88
- try {
89
- message = gatewayMessage(JSON.parse(data));
90
- }
91
- catch {
92
- return;
93
- }
94
- if (message)
95
- options.onMessage(message);
96
- });
97
- current.addEventListener("close", ({ code }) => finish(code));
98
- current.addEventListener("error", () => {
99
- finish();
100
- current.close();
101
- });
102
- };
103
- void open();
104
- return {
105
- send(envelope) {
106
- if (stopped || !socket || socket.readyState !== 1 || !outboundEnvelope(envelope))
107
- return false;
108
- let text;
109
- try {
110
- text = JSON.stringify(envelope);
111
- if (!outboundEnvelope(JSON.parse(text)))
112
- return false;
113
- }
114
- catch {
115
- return false;
116
- }
117
- const size = byteLength(text);
118
- if (size > MAX_MESSAGE_BYTES)
119
- return false;
120
- if (socket.bufferedAmount + size > MAX_CLIENT_BUFFER_BYTES) {
121
- socket.close(CLOSE.outputLimit, "outbound buffer limit");
122
- return false;
123
- }
124
- try {
125
- socket.send(text);
126
- return true;
127
- }
128
- catch {
129
- return false;
130
- }
131
- },
132
- stop() {
133
- stopped = true;
134
- clearTimeout(timer);
135
- clearTimeout(connectingTimer);
136
- clearTimeout(stableTimer);
137
- const previous = socket;
138
- socket = null;
139
- previous?.close(1000, "client stopped");
140
- status("stopped");
141
- },
142
- };
143
- }
package/dist/cloud.d.ts DELETED
@@ -1,7 +0,0 @@
1
- export declare class CloudError extends Error {
2
- readonly code: string;
3
- constructor(code: string);
4
- }
5
- export declare function cloudOrigin(value: string): string;
6
- export declare function cloudRequest(origin: string, path: string, token: string, method?: string): Promise<unknown>;
7
- export declare function gatewayConfig(origin: string, token: string): Promise<string>;