@rome-os/node-core 0.1.0 → 0.1.2

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.
@@ -1,6 +1,6 @@
1
1
  import WebSocket from "ws";
2
2
  import { isRecord } from "./actions.js";
3
- import { DAEMON_PROTOCOL_VERSION, parseDaemonStatus, } from "./daemon-protocol.js";
3
+ import { DAEMON_PROTOCOL_VERSION, encodeBinaryMessage, MAX_LOCAL_MESSAGE_BYTES, parseBinaryMessage, parseDaemonStatus, } from "./daemon-protocol.js";
4
4
  export class DaemonVersionError extends Error {
5
5
  constructor() {
6
6
  super("The caller daemon uses an incompatible protocol. Stop it explicitly before restarting with this version.");
@@ -52,26 +52,47 @@ export class RpcConnection {
52
52
  });
53
53
  socket.on("message", (data, binary) => {
54
54
  let message;
55
- try {
56
- message = JSON.parse(data.toString());
55
+ let body = new Uint8Array();
56
+ if (binary) {
57
+ const parsed = parseBinaryMessage(Buffer.isBuffer(data)
58
+ ? data
59
+ : Array.isArray(data)
60
+ ? Buffer.concat(data)
61
+ : Buffer.from(data));
62
+ if (!parsed) {
63
+ this.close();
64
+ return;
65
+ }
66
+ ({ message, body } = parsed);
57
67
  }
58
- catch {
59
- this.close();
60
- return;
68
+ else {
69
+ try {
70
+ message = JSON.parse(data.toString());
71
+ }
72
+ catch {
73
+ this.close();
74
+ return;
75
+ }
61
76
  }
62
- if (binary || !isRecord(message) || message.jsonrpc !== "2.0") {
77
+ if (!isRecord(message) || message.jsonrpc !== "2.0") {
63
78
  this.close();
64
79
  return;
65
80
  }
66
81
  if (typeof message.method === "string" && !Object.hasOwn(message, "id")) {
67
- this.onNotification?.(message.method, message.params);
82
+ const params = message.params;
83
+ if (message.method === "events.transfer") {
84
+ if (isRecord(params) && typeof params.requestId === "string")
85
+ this.pending.get(params.requestId)?.progress?.(params);
86
+ return;
87
+ }
88
+ this.onNotification?.(message.method, params);
68
89
  return;
69
90
  }
70
91
  const pending = typeof message.id === "string" ? this.pending.get(message.id) : undefined;
71
92
  if (!pending)
72
93
  return;
73
94
  if (Object.hasOwn(message, "result") && !Object.hasOwn(message, "error"))
74
- pending.resolve(message.result);
95
+ pending.resolve(message.result, body);
75
96
  else if (!Object.hasOwn(message, "result") &&
76
97
  isRecord(message.error) &&
77
98
  Number.isInteger(message.error.code) &&
@@ -84,11 +105,18 @@ export class RpcConnection {
84
105
  get open() {
85
106
  return this.socket.readyState === WebSocket.OPEN;
86
107
  }
87
- request(method, params = {}, timeoutMs = 85_000) {
108
+ async request(method, params = {}, timeoutMs = 85_000, progress) {
109
+ return (await this.submit(method, params, undefined, timeoutMs, progress)).result;
110
+ }
111
+ async requestBinary(method, params, body, timeoutMs = 85_000) {
112
+ return this.submit(method, params, body, timeoutMs);
113
+ }
114
+ submit(method, params, body, timeoutMs, progress) {
88
115
  if (!this.open)
89
116
  return Promise.reject(new DaemonRequestError());
90
117
  const id = String(++this.nextId);
91
- const text = JSON.stringify({ jsonrpc: "2.0", id, method, params });
118
+ const message = { jsonrpc: "2.0", id, method, params };
119
+ const data = body ? encodeBinaryMessage(message, body) : JSON.stringify(message);
92
120
  return new Promise((resolve, reject) => {
93
121
  const finish = (error, value) => {
94
122
  clearTimeout(timer);
@@ -98,13 +126,16 @@ export class RpcConnection {
98
126
  else
99
127
  resolve(value);
100
128
  };
101
- const timer = setTimeout(() => finish(new DaemonRequestError()), timeoutMs);
129
+ const timer = Number.isFinite(timeoutMs)
130
+ ? setTimeout(() => finish(new DaemonRequestError()), timeoutMs)
131
+ : undefined;
102
132
  this.pending.set(id, {
103
- resolve: (value) => finish(undefined, value),
133
+ resolve: (result, body) => finish(undefined, { result, body }),
104
134
  reject: (error) => finish(error),
135
+ progress,
105
136
  });
106
137
  try {
107
- this.socket.send(text, (error) => {
138
+ this.socket.send(data, { binary: Boolean(body) }, (error) => {
108
139
  if (error)
109
140
  finish(new DaemonRequestError());
110
141
  });
@@ -118,12 +149,13 @@ export class RpcConnection {
118
149
  this.socket.terminate();
119
150
  }
120
151
  }
121
- export async function connectRpc(state, signal) {
152
+ export async function connectRpc(state, signal, protocolVersion = DAEMON_PROTOCOL_VERSION) {
122
153
  signal?.throwIfAborted();
123
154
  const socket = new WebSocket(`ws://127.0.0.1:${state.port}/rpc`, {
124
155
  headers: { authorization: `Bearer ${state.token}` },
125
156
  handshakeTimeout: 1500,
126
157
  perMessageDeflate: false,
158
+ maxPayload: MAX_LOCAL_MESSAGE_BYTES,
127
159
  followRedirects: false,
128
160
  });
129
161
  const rpc = new RpcConnection(socket);
@@ -150,8 +182,8 @@ export async function connectRpc(state, signal) {
150
182
  if (signal?.aborted)
151
183
  abort();
152
184
  });
153
- const value = await rpc.request("daemon.hello", { protocolVersion: DAEMON_PROTOCOL_VERSION }, 1500);
154
- if (isRecord(value) && value.protocolVersion !== DAEMON_PROTOCOL_VERSION)
185
+ const value = await rpc.request("daemon.hello", { protocolVersion }, 1500);
186
+ if (isRecord(value) && value.protocolVersion !== protocolVersion)
155
187
  throw new DaemonVersionError();
156
188
  const status = parseDaemonStatus(value);
157
189
  if (!status || status.pid !== state.pid)
package/dist/daemon.js CHANGED
@@ -1,13 +1,29 @@
1
1
  import { randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import WebSocket, { WebSocketServer } from "ws";
4
- import { isRecord } from "./actions.js";
4
+ import { actionError, isRecord } from "./actions.js";
5
5
  import { DeviceConnector } from "./connector.js";
6
6
  import { daemonStatePath, readCallerCredential } from "./local.js";
7
7
  import { writePrivateJson } from "./storage.js";
8
- import { DAEMON_PROTOCOL_VERSION } from "./daemon-protocol.js";
8
+ import { DAEMON_PROTOCOL_VERSION, encodeBinaryMessage, MAX_LOCAL_MESSAGE_BYTES, parseBinaryMessage, } from "./daemon-protocol.js";
9
9
  import { DeviceStatusReader } from "./devices-status.js";
10
10
  import { RpcError } from "./daemon-rpc.js";
11
+ import { isAbsolute } from "node:path";
12
+ import { isUuid } from "./frame.js";
13
+ import { copyWithDevice, TRANSFER_VERSION, TransferError } from "./transfer.js";
14
+ function copyRequest(params) {
15
+ const { direction, localPath, deviceId, remotePath } = params;
16
+ if ((direction !== "push" && direction !== "pull") ||
17
+ typeof localPath !== "string" ||
18
+ !isAbsolute(localPath) ||
19
+ localPath.includes("\0") ||
20
+ !isUuid(deviceId) ||
21
+ typeof remotePath !== "string" ||
22
+ !remotePath ||
23
+ remotePath.includes("\0"))
24
+ return null;
25
+ return { direction, localPath, deviceId: deviceId.toLowerCase(), remotePath };
26
+ }
11
27
  export async function serveDaemon(config, signal) {
12
28
  if (signal.aborted)
13
29
  return;
@@ -44,7 +60,11 @@ export async function serveDaemon(config, signal) {
44
60
  res.writeHead(authorized(req) ? 426 : 401, { "cache-control": "no-store" }).end();
45
61
  });
46
62
  server.headersTimeout = 10_000;
47
- const sockets = new WebSocketServer({ noServer: true, perMessageDeflate: false });
63
+ const sockets = new WebSocketServer({
64
+ noServer: true,
65
+ perMessageDeflate: false,
66
+ maxPayload: MAX_LOCAL_MESSAGE_BYTES,
67
+ });
48
68
  server.on("upgrade", (req, socket, head) => {
49
69
  if (closing || req.url !== "/rpc" || req.headers.origin || !authorized(req)) {
50
70
  socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
@@ -60,12 +80,46 @@ export async function serveDaemon(config, signal) {
60
80
  peer.socket.terminate();
61
81
  });
62
82
  }
83
+ function deliver(peer, reply) {
84
+ if (!reply.body) {
85
+ send(peer, reply.message);
86
+ return;
87
+ }
88
+ if (peer.socket.readyState !== WebSocket.OPEN)
89
+ return;
90
+ let data = encodeBinaryMessage(reply.message, reply.body);
91
+ if (data.byteLength > MAX_LOCAL_MESSAGE_BYTES) {
92
+ const id = isRecord(reply.message) ? reply.message.id : null;
93
+ data = encodeBinaryMessage({
94
+ jsonrpc: "2.0",
95
+ id,
96
+ result: actionError("message_too_large", "The response exceeds the local message limit. The action may have completed."),
97
+ }, new Uint8Array());
98
+ }
99
+ peer.socket.send(data, { binary: true }, (error) => {
100
+ if (error)
101
+ peer.socket.terminate();
102
+ });
103
+ }
63
104
  function notify(peer) {
64
- if (peer.socket.bufferedAmount > 1024 * 1024) {
105
+ if (peer.eventBytes > 1024 * 1024) {
65
106
  peer.socket.terminate();
66
107
  return;
67
108
  }
68
- send(peer, { jsonrpc: "2.0", method: "events.connection", params: snapshot() });
109
+ if (peer.socket.readyState !== WebSocket.OPEN)
110
+ return;
111
+ const text = JSON.stringify({
112
+ jsonrpc: "2.0",
113
+ method: "events.connection",
114
+ params: snapshot(),
115
+ });
116
+ const size = Buffer.byteLength(text);
117
+ peer.eventBytes += size;
118
+ peer.socket.send(text, (error) => {
119
+ peer.eventBytes -= size;
120
+ if (error)
121
+ peer.socket.terminate();
122
+ });
69
123
  }
70
124
  const close = () => {
71
125
  if (closing)
@@ -86,7 +140,7 @@ export async function serveDaemon(config, signal) {
86
140
  function error(id, code, message) {
87
141
  return { jsonrpc: "2.0", id, error: { code, message } };
88
142
  }
89
- async function dispatch(peer, value) {
143
+ async function dispatch(peer, value, input) {
90
144
  if (!isRecord(value) ||
91
145
  value.jsonrpc !== "2.0" ||
92
146
  typeof value.method !== "string" ||
@@ -94,7 +148,7 @@ export async function serveDaemon(config, signal) {
94
148
  typeof value.id !== "string" &&
95
149
  typeof value.id !== "number" &&
96
150
  value.id !== null))
97
- return error(null, -32600, "Invalid Request");
151
+ return { message: error(null, -32600, "Invalid Request") };
98
152
  const hasId = Object.hasOwn(value, "id");
99
153
  const id = value.id;
100
154
  if (hasId && id !== null && peer.pending.has(id)) {
@@ -109,7 +163,10 @@ export async function serveDaemon(config, signal) {
109
163
  const params = Object.hasOwn(value, "params") ? value.params : {};
110
164
  if (!isRecord(params))
111
165
  throw new RpcError(-32602, "Invalid params");
166
+ if ((value.method === "devices.runBinary") !== Boolean(input))
167
+ throw new RpcError(-32600, "Only devices.runBinary uses binary messages, and it requires one.");
112
168
  let result;
169
+ let body;
113
170
  if (value.method === "daemon.hello") {
114
171
  if (params.protocolVersion !== DAEMON_PROTOCOL_VERSION)
115
172
  throw new RpcError(-32002, "Incompatible daemon protocol.");
@@ -138,6 +195,68 @@ export async function serveDaemon(config, signal) {
138
195
  throw new RpcError(-32602, "A device ID and action are required.");
139
196
  result = await connector.run(params.deviceId, params.action, params.args ?? {});
140
197
  break;
198
+ case "devices.runBinary": {
199
+ if (typeof params.deviceId !== "string" || typeof params.action !== "string")
200
+ throw new RpcError(-32602, "A device ID and action are required.");
201
+ const reply = await connector.runBinary(params.deviceId, params.action, params.args ?? {}, input ?? new Uint8Array());
202
+ result = reply.response;
203
+ body = reply.body;
204
+ break;
205
+ }
206
+ case "files.copy": {
207
+ const request = copyRequest(params);
208
+ if (!request)
209
+ throw new RpcError(-32602, "files.copy requires direction push or pull, an absolute localPath, a device UUID, and remotePath.");
210
+ const controller = new AbortController();
211
+ peer.copies.add(controller);
212
+ if (peer.socket.readyState !== WebSocket.OPEN)
213
+ controller.abort();
214
+ const canceled = () => actionError("canceled", "The client disconnected. The copy did not start.");
215
+ let last = 0;
216
+ try {
217
+ if (controller.signal.aborted) {
218
+ result = canceled();
219
+ break;
220
+ }
221
+ const info = await connector.run(request.deviceId, "system.info", {});
222
+ if (controller.signal.aborted) {
223
+ result = canceled();
224
+ break;
225
+ }
226
+ if (!info.ok) {
227
+ result = info;
228
+ break;
229
+ }
230
+ if (!isRecord(info.result) || info.result.transferVersion !== TRANSFER_VERSION) {
231
+ result = actionError("unsupported_device", "The device runs a rome-node version without file transfers. Update rome-node on that device.");
232
+ break;
233
+ }
234
+ const summary = await copyWithDevice((events) => connector.openChannel(request.deviceId, events), request, {
235
+ signal: controller.signal,
236
+ onProgress: ({ bytes, total }) => {
237
+ const now = Date.now();
238
+ if (now - last < 250 && bytes !== total)
239
+ return;
240
+ last = now;
241
+ send(peer, {
242
+ jsonrpc: "2.0",
243
+ method: "events.transfer",
244
+ params: { requestId: id, bytes, total },
245
+ });
246
+ },
247
+ });
248
+ result = { type: "response", ok: true, result: summary };
249
+ }
250
+ catch (cause) {
251
+ if (!(cause instanceof TransferError))
252
+ throw cause;
253
+ result = actionError(cause.code, cause.message);
254
+ }
255
+ finally {
256
+ peer.copies.delete(controller);
257
+ }
258
+ break;
259
+ }
141
260
  case "events.subscribe":
142
261
  case "events.unsubscribe":
143
262
  if (params.topic !== "connection")
@@ -151,14 +270,16 @@ export async function serveDaemon(config, signal) {
151
270
  throw new RpcError(-32601, "Method not found");
152
271
  }
153
272
  }
154
- return hasId ? { jsonrpc: "2.0", id, result } : undefined;
273
+ return hasId ? { message: { jsonrpc: "2.0", id, result }, body } : undefined;
155
274
  }
156
275
  catch (cause) {
157
276
  if (!hasId)
158
277
  return;
159
- return cause instanceof RpcError
160
- ? error(id, cause.code, cause.message)
161
- : error(id, -32000, "The device service is unavailable.");
278
+ return {
279
+ message: cause instanceof RpcError
280
+ ? error(id, cause.code, cause.message)
281
+ : error(id, -32000, "The device service is unavailable."),
282
+ };
162
283
  }
163
284
  finally {
164
285
  if (hasId && id !== null)
@@ -166,14 +287,41 @@ export async function serveDaemon(config, signal) {
166
287
  }
167
288
  }
168
289
  sockets.on("connection", (socket) => {
169
- const peer = { socket, ready: false, subscribed: false, alive: true, pending: new Set() };
290
+ const peer = {
291
+ socket,
292
+ ready: false,
293
+ subscribed: false,
294
+ alive: true,
295
+ pending: new Set(),
296
+ eventBytes: 0,
297
+ copies: new Set(),
298
+ };
170
299
  peers.add(peer);
171
300
  socket.on("error", () => socket.terminate());
172
- socket.on("close", () => peers.delete(peer));
301
+ socket.on("close", () => {
302
+ peers.delete(peer);
303
+ for (const copy of peer.copies)
304
+ copy.abort();
305
+ });
173
306
  socket.on("pong", () => {
174
307
  peer.alive = true;
175
308
  });
176
309
  socket.on("message", (data, binary) => {
310
+ if (binary) {
311
+ const parsed = parseBinaryMessage(Buffer.isBuffer(data)
312
+ ? data
313
+ : Array.isArray(data)
314
+ ? Buffer.concat(data)
315
+ : Buffer.from(data));
316
+ if (!parsed) {
317
+ send(peer, error(null, -32600, "Invalid Request"));
318
+ return;
319
+ }
320
+ void dispatch(peer, parsed.message, parsed.body)
321
+ .then((reply) => reply && deliver(peer, reply))
322
+ .catch(() => socket.terminate());
323
+ return;
324
+ }
177
325
  let request;
178
326
  try {
179
327
  request = JSON.parse(data.toString());
@@ -182,20 +330,18 @@ export async function serveDaemon(config, signal) {
182
330
  send(peer, error(null, -32700, "Parse error"));
183
331
  return;
184
332
  }
185
- if (binary) {
186
- send(peer, error(null, -32600, "Invalid Request"));
187
- return;
188
- }
189
333
  void (async () => {
190
334
  if (Array.isArray(request) && request.length > 0) {
191
- const responses = (await Promise.all(request.map((item) => dispatch(peer, item)))).filter((value) => value !== undefined);
335
+ const responses = (await Promise.all(request.map((item) => dispatch(peer, item))))
336
+ .filter((reply) => reply !== undefined)
337
+ .map((reply) => reply.message);
192
338
  if (responses.length)
193
339
  send(peer, responses);
194
340
  }
195
341
  else {
196
- const response = await dispatch(peer, request);
197
- if (response !== undefined)
198
- send(peer, response);
342
+ const reply = await dispatch(peer, request);
343
+ if (reply !== undefined)
344
+ send(peer, reply.message);
199
345
  }
200
346
  })().catch(() => socket.terminate());
201
347
  });
@@ -0,0 +1,8 @@
1
+ import { type DeviceSession } from "./device-session.js";
2
+ export interface DeviceAuthorizationPrompt {
3
+ verificationUri: string;
4
+ verificationUriComplete: string;
5
+ userCode: string;
6
+ expiresIn: number;
7
+ }
8
+ export declare function loginDeviceCode(cloudUrl: string, name: string, signal: AbortSignal, onAuthorizationRequired: (prompt: DeviceAuthorizationPrompt) => void): Promise<DeviceSession>;
@@ -0,0 +1,156 @@
1
+ import { isRecord } from "./actions.js";
2
+ import { cloudOrigin } from "./cloud.js";
3
+ import { devicePlatform, deviceSessionFromToken } from "./device-session.js";
4
+ class DeviceAuthorizationError extends Error {
5
+ }
6
+ class RetryableRequestError extends Error {
7
+ }
8
+ function seconds(value) {
9
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 2_147_483;
10
+ }
11
+ function verificationUrl(value, origin) {
12
+ if (typeof value !== "string")
13
+ throw new DeviceAuthorizationError("Invalid device authorization response.");
14
+ let url;
15
+ try {
16
+ url = new URL(value);
17
+ }
18
+ catch {
19
+ throw new DeviceAuthorizationError("Invalid device authorization response.");
20
+ }
21
+ if (url.origin !== origin || url.username || url.password)
22
+ throw new DeviceAuthorizationError("Invalid device authorization response.");
23
+ return url.href;
24
+ }
25
+ async function post(origin, path, body, signal) {
26
+ let response;
27
+ try {
28
+ response = await fetch(new URL(path, origin), {
29
+ method: "POST",
30
+ body,
31
+ signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
32
+ redirect: "error",
33
+ cache: "no-store",
34
+ });
35
+ }
36
+ catch {
37
+ throw new RetryableRequestError();
38
+ }
39
+ if (response.status === 429 || response.status >= 500) {
40
+ await response.body?.cancel().catch(() => { });
41
+ throw new RetryableRequestError();
42
+ }
43
+ let result;
44
+ try {
45
+ result = await response.json();
46
+ }
47
+ catch (error) {
48
+ if (error instanceof SyntaxError)
49
+ throw new DeviceAuthorizationError("Invalid device authorization response.");
50
+ throw new RetryableRequestError();
51
+ }
52
+ return { response, result };
53
+ }
54
+ function wait(milliseconds, signal) {
55
+ return new Promise((resolve, reject) => {
56
+ signal.throwIfAborted();
57
+ const abort = () => {
58
+ clearTimeout(timer);
59
+ reject(signal.reason);
60
+ };
61
+ const timer = setTimeout(() => {
62
+ signal.removeEventListener("abort", abort);
63
+ resolve();
64
+ }, milliseconds);
65
+ signal.addEventListener("abort", abort, { once: true });
66
+ });
67
+ }
68
+ export async function loginDeviceCode(cloudUrl, name, signal, onAuthorizationRequired) {
69
+ const origin = cloudOrigin(cloudUrl);
70
+ const expiry = new AbortController();
71
+ const pollingSignal = AbortSignal.any([signal, expiry.signal]);
72
+ let timer;
73
+ try {
74
+ signal.throwIfAborted();
75
+ const { response, result } = await post(origin, "/oauth2/device_authorization", new URLSearchParams({
76
+ client_id: "rome-computer",
77
+ display_name: name,
78
+ platform: devicePlatform(),
79
+ }), signal);
80
+ if (!response.ok ||
81
+ !isRecord(result) ||
82
+ typeof result.device_code !== "string" ||
83
+ !result.device_code ||
84
+ typeof result.user_code !== "string" ||
85
+ !/^[A-Za-z0-9-]{1,64}$/.test(result.user_code) ||
86
+ !seconds(result.expires_in) ||
87
+ !seconds(result.interval))
88
+ throw new DeviceAuthorizationError("Invalid device authorization response.");
89
+ const prompt = {
90
+ verificationUri: verificationUrl(result.verification_uri, origin),
91
+ verificationUriComplete: verificationUrl(result.verification_uri_complete, origin),
92
+ userCode: result.user_code,
93
+ expiresIn: result.expires_in,
94
+ };
95
+ const deadline = Date.now() + result.expires_in * 1000;
96
+ timer = setTimeout(() => expiry.abort(), result.expires_in * 1000);
97
+ onAuthorizationRequired(prompt);
98
+ let interval = result.interval * 1000;
99
+ while (true) {
100
+ await wait(Math.min(interval, Math.max(0, deadline - Date.now())), pollingSignal);
101
+ if (Date.now() >= deadline)
102
+ expiry.abort();
103
+ pollingSignal.throwIfAborted();
104
+ let polled;
105
+ try {
106
+ polled = await post(origin, "/oauth2/token", new URLSearchParams({
107
+ client_id: "rome-computer",
108
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
109
+ device_code: result.device_code,
110
+ }), pollingSignal);
111
+ }
112
+ catch (error) {
113
+ pollingSignal.throwIfAborted();
114
+ if (!(error instanceof RetryableRequestError))
115
+ throw error;
116
+ interval = Math.max(interval, Math.min(interval * 2, 60_000));
117
+ continue;
118
+ }
119
+ const { response, result: token } = polled;
120
+ pollingSignal.throwIfAborted();
121
+ if (response.ok) {
122
+ const session = deviceSessionFromToken(token, origin);
123
+ if (!session)
124
+ throw new DeviceAuthorizationError("Invalid token response.");
125
+ return session;
126
+ }
127
+ if (response.status === 400 && isRecord(token)) {
128
+ if (token.error === "authorization_pending")
129
+ continue;
130
+ if (token.error === "slow_down") {
131
+ interval += 5000;
132
+ continue;
133
+ }
134
+ if (token.error === "access_denied")
135
+ throw new DeviceAuthorizationError("Device authorization denied.");
136
+ if (token.error === "expired_token")
137
+ throw new DeviceAuthorizationError("Device authorization expired.");
138
+ if (token.error === "invalid_grant")
139
+ throw new DeviceAuthorizationError("Device authorization is invalid.");
140
+ }
141
+ throw new DeviceAuthorizationError("Device authorization rejected.");
142
+ }
143
+ }
144
+ catch (error) {
145
+ if (signal.aborted)
146
+ throw new DeviceAuthorizationError("Authorization canceled.");
147
+ if (expiry.signal.aborted)
148
+ throw new DeviceAuthorizationError("Device authorization expired.");
149
+ if (error instanceof DeviceAuthorizationError)
150
+ throw error;
151
+ throw new DeviceAuthorizationError("Could not complete device authorization. Try connecting again.");
152
+ }
153
+ finally {
154
+ clearTimeout(timer);
155
+ }
156
+ }
@@ -0,0 +1,7 @@
1
+ export interface DeviceSession {
2
+ cloudUrl: string;
3
+ token: string;
4
+ deviceId: string;
5
+ }
6
+ export declare function devicePlatform(): "macos" | "windows" | "linux";
7
+ export declare function deviceSessionFromToken(body: unknown, origin: string): DeviceSession | null;
@@ -0,0 +1,16 @@
1
+ import { platform } from "node:os";
2
+ import { isRecord } from "./actions.js";
3
+ import { validId } from "./protocol.js";
4
+ export function devicePlatform() {
5
+ const value = platform();
6
+ return value === "darwin" ? "macos" : value === "win32" ? "windows" : "linux";
7
+ }
8
+ export function deviceSessionFromToken(body, origin) {
9
+ if (!isRecord(body) ||
10
+ typeof body.access_token !== "string" ||
11
+ !/^romedev_[A-Za-z0-9_-]{43}$/.test(body.access_token) ||
12
+ body.token_type !== "Bearer" ||
13
+ !validId(body.device_id))
14
+ return null;
15
+ return { cloudUrl: origin, token: body.access_token, deviceId: body.device_id };
16
+ }
@@ -1,5 +1,8 @@
1
+ import { type Frame } from "./frame.js";
1
2
  import type { InboundEnvelope, OutboundEnvelope } from "./protocol.js";
2
- export declare function createExecutor(name: string, send: (message: OutboundEnvelope) => boolean, secrets?: string[]): {
3
+ export declare function redactBytes(data: Buffer, secrets: string[]): Buffer;
4
+ export declare function createExecutor(name: string, send: (message: OutboundEnvelope) => boolean, secrets?: string[], sendFrame?: (frame: Frame) => boolean): {
3
5
  receive(message: InboundEnvelope): Promise<void>;
6
+ receiveFrame(frame: Frame): Promise<void>;
4
7
  disconnect(): void;
5
8
  };