@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.
- package/README.md +42 -1
- package/dist/actions.d.ts +4 -0
- package/dist/client.d.ts +4 -1
- package/dist/client.js +30 -1
- package/dist/connect.js +2 -3
- package/dist/connector.d.ts +10 -1
- package/dist/connector.js +129 -7
- package/dist/daemon-client.d.ts +8 -4
- package/dist/daemon-client.js +86 -2
- package/dist/daemon-control.js +13 -2
- package/dist/daemon-protocol.d.ts +22 -1
- package/dist/daemon-protocol.js +22 -1
- package/dist/daemon-rpc.d.ts +7 -2
- package/dist/daemon-rpc.js +49 -17
- package/dist/daemon.js +167 -21
- package/dist/device-authorization.d.ts +8 -0
- package/dist/device-authorization.js +156 -0
- package/dist/device-session.d.ts +7 -0
- package/dist/device-session.js +16 -0
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +180 -66
- package/dist/frame.d.ts +24 -0
- package/dist/frame.js +64 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/login.d.ts +2 -5
- package/dist/login.js +5 -8
- package/dist/transfer.d.ts +102 -0
- package/dist/transfer.js +518 -0
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -1,77 +1,142 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { platform } from "node:os";
|
|
3
3
|
import { actionError, isRecord } from "./actions.js";
|
|
4
|
-
|
|
4
|
+
import { decodeMeta, encodeMeta, FRAME_TYPE, frameFits, MAX_FRAME_BYTES, } from "./frame.js";
|
|
5
|
+
import { parseTransferMessage, TRANSFER_VERSION, TransferHost } from "./transfer.js";
|
|
6
|
+
function execInput(input) {
|
|
7
|
+
if (!isRecord(input) ||
|
|
8
|
+
typeof input.command !== "string" ||
|
|
9
|
+
!input.command ||
|
|
10
|
+
input.command.includes("\0") ||
|
|
11
|
+
(input.args !== undefined &&
|
|
12
|
+
(!Array.isArray(input.args) ||
|
|
13
|
+
!input.args.every((arg) => typeof arg === "string" && !arg.includes("\0")))) ||
|
|
14
|
+
(input.cwd !== undefined && (typeof input.cwd !== "string" || input.cwd.includes("\0"))))
|
|
15
|
+
return null;
|
|
16
|
+
return {
|
|
17
|
+
command: input.command,
|
|
18
|
+
args: (input.args ?? []),
|
|
19
|
+
cwd: input.cwd,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function redactBytes(data, secrets) {
|
|
23
|
+
for (const secret of secrets) {
|
|
24
|
+
const needle = Buffer.from(secret);
|
|
25
|
+
if (!needle.byteLength)
|
|
26
|
+
continue;
|
|
27
|
+
for (let at = data.indexOf(needle); at !== -1; at = data.indexOf(needle, at + needle.length))
|
|
28
|
+
data.fill(0x2a, at, at + needle.byteLength);
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
export function createExecutor(name, send, secrets = [], sendFrame = () => false) {
|
|
5
33
|
const children = new Set();
|
|
34
|
+
const transfers = new TransferHost();
|
|
6
35
|
let generation = 0;
|
|
7
|
-
const
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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."));
|
|
36
|
+
const info = () => ({
|
|
37
|
+
type: "response",
|
|
38
|
+
ok: true,
|
|
39
|
+
result: {
|
|
40
|
+
name,
|
|
41
|
+
platform: platform() === "darwin" ? "macos" : platform() === "win32" ? "windows" : platform(),
|
|
42
|
+
actions: ["system.info", "exec"],
|
|
43
|
+
frameVersion: 1,
|
|
44
|
+
transferVersion: TRANSFER_VERSION,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const invalidExec = () => actionError("invalid_args", "exec requires command, optional string args[], and optional cwd.");
|
|
48
|
+
function runProgram({ command, args, cwd }, stdin) {
|
|
49
|
+
if (children.size >= 8)
|
|
50
|
+
return Promise.resolve(actionError("busy", "The computer is already running eight programs."));
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(?:ROME_.*(?:TOKEN|SECRET|CREDENTIAL)|NODE_OPTIONS)$/i.test(key)));
|
|
53
|
+
let child;
|
|
54
|
+
try {
|
|
55
|
+
child = spawn(command, args, {
|
|
56
|
+
cwd,
|
|
57
|
+
env: environment,
|
|
58
|
+
shell: false,
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
stdio: [stdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
58
61
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
resolve(actionError("exec_failed", "Could not start the program."));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
children.add(child);
|
|
68
|
+
if (stdin && child.stdin) {
|
|
69
|
+
child.stdin.on("error", () => { });
|
|
70
|
+
child.stdin.end(stdin);
|
|
71
|
+
}
|
|
72
|
+
const output = { stdout: [], stderr: [] };
|
|
73
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
74
|
+
child[stream]?.on("data", (chunk) => {
|
|
75
|
+
output[stream].push(chunk);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
child.once("error", () => {
|
|
79
|
+
children.delete(child);
|
|
80
|
+
resolve(actionError("exec_failed", "Could not start the program. Check the executable and working directory."));
|
|
81
|
+
});
|
|
82
|
+
child.once("close", (exitCode, signal) => {
|
|
83
|
+
children.delete(child);
|
|
84
|
+
resolve({
|
|
85
|
+
exitCode,
|
|
86
|
+
signal,
|
|
87
|
+
stdout: Buffer.concat(output.stdout),
|
|
88
|
+
stderr: Buffer.concat(output.stderr),
|
|
73
89
|
});
|
|
74
90
|
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const redactText = (data) => secrets.reduce((text, secret) => text.replaceAll(secret, "[redacted]"), data.toString("utf8"));
|
|
94
|
+
const isOutput = (value) => !("type" in value);
|
|
95
|
+
const handlers = {
|
|
96
|
+
"system.info": async () => info(),
|
|
97
|
+
exec: async (input) => {
|
|
98
|
+
const parsed = execInput(input);
|
|
99
|
+
if (!parsed)
|
|
100
|
+
return invalidExec();
|
|
101
|
+
const output = await runProgram(parsed);
|
|
102
|
+
if (!isOutput(output))
|
|
103
|
+
return output;
|
|
104
|
+
return {
|
|
105
|
+
type: "response",
|
|
106
|
+
ok: true,
|
|
107
|
+
result: {
|
|
108
|
+
exitCode: output.exitCode,
|
|
109
|
+
signal: output.signal,
|
|
110
|
+
stdout: redactText(output.stdout),
|
|
111
|
+
stderr: redactText(output.stderr),
|
|
112
|
+
truncated: { stdout: false, stderr: false },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const empty = new Uint8Array();
|
|
118
|
+
const frameHandlers = {
|
|
119
|
+
"system.info": async () => ({ response: info(), body: empty }),
|
|
120
|
+
exec: async (input, body) => {
|
|
121
|
+
const parsed = execInput(input);
|
|
122
|
+
if (!parsed)
|
|
123
|
+
return { response: invalidExec(), body: empty };
|
|
124
|
+
const output = await runProgram(parsed, body);
|
|
125
|
+
if (!isOutput(output))
|
|
126
|
+
return { response: output, body: empty };
|
|
127
|
+
return {
|
|
128
|
+
response: {
|
|
129
|
+
type: "response",
|
|
130
|
+
ok: true,
|
|
131
|
+
result: {
|
|
132
|
+
exitCode: output.exitCode,
|
|
133
|
+
signal: output.signal,
|
|
134
|
+
stderr: redactText(output.stderr),
|
|
135
|
+
truncated: { stdout: false, stderr: false },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
body: redactBytes(output.stdout, secrets),
|
|
139
|
+
};
|
|
75
140
|
},
|
|
76
141
|
};
|
|
77
142
|
return {
|
|
@@ -90,8 +155,57 @@ export function createExecutor(name, send, secrets = []) {
|
|
|
90
155
|
if (current === generation)
|
|
91
156
|
send(reply);
|
|
92
157
|
},
|
|
158
|
+
async receiveFrame(frame) {
|
|
159
|
+
if (frame.type !== FRAME_TYPE.request)
|
|
160
|
+
return;
|
|
161
|
+
const request = decodeMeta(frame.meta);
|
|
162
|
+
const transfer = parseTransferMessage(request);
|
|
163
|
+
if (transfer) {
|
|
164
|
+
transfers.receive(frame.id, frame.peer, transfer, frame.body);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!isRecord(request) || request.type !== "request" || typeof request.action !== "string")
|
|
168
|
+
return;
|
|
169
|
+
const current = generation;
|
|
170
|
+
if (request.action === "transfer.open") {
|
|
171
|
+
void transfers.open(frame.id, frame.peer, request.args, (meta, body = empty) => {
|
|
172
|
+
if (current !== generation)
|
|
173
|
+
return false;
|
|
174
|
+
return sendFrame({
|
|
175
|
+
type: FRAME_TYPE.response,
|
|
176
|
+
id: frame.id,
|
|
177
|
+
peer: frame.peer,
|
|
178
|
+
meta: encodeMeta(meta),
|
|
179
|
+
body,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const handler = Object.hasOwn(frameHandlers, request.action)
|
|
185
|
+
? frameHandlers[request.action]
|
|
186
|
+
: undefined;
|
|
187
|
+
const result = handler
|
|
188
|
+
? await handler(request.args, frame.body).catch(() => ({
|
|
189
|
+
response: actionError("exec_failed", "The action failed."),
|
|
190
|
+
body: empty,
|
|
191
|
+
}))
|
|
192
|
+
: {
|
|
193
|
+
response: actionError("unsupported_action", "The computer does not support this action."),
|
|
194
|
+
body: empty,
|
|
195
|
+
};
|
|
196
|
+
if (current !== generation)
|
|
197
|
+
return;
|
|
198
|
+
let meta = encodeMeta(result.response);
|
|
199
|
+
let body = result.body;
|
|
200
|
+
if (!frameFits(meta.byteLength, body.byteLength)) {
|
|
201
|
+
meta = encodeMeta(actionError("output_too_large", `The output does not fit in one ${MAX_FRAME_BYTES / 1024 / 1024} MiB frame and was discarded. The program ran. Use rome-node cp to copy large files.`));
|
|
202
|
+
body = empty;
|
|
203
|
+
}
|
|
204
|
+
sendFrame({ type: FRAME_TYPE.response, id: frame.id, peer: frame.peer, meta, body });
|
|
205
|
+
},
|
|
93
206
|
disconnect() {
|
|
94
207
|
generation++;
|
|
208
|
+
transfers.abortAll();
|
|
95
209
|
for (const child of children) {
|
|
96
210
|
child.kill("SIGTERM");
|
|
97
211
|
const timer = setTimeout(() => {
|
package/dist/frame.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const FRAME_VERSION = 1;
|
|
2
|
+
export declare const FRAME_TYPE: {
|
|
3
|
+
readonly request: 1;
|
|
4
|
+
readonly response: 2;
|
|
5
|
+
readonly routeError: 3;
|
|
6
|
+
};
|
|
7
|
+
export type FrameType = (typeof FRAME_TYPE)[keyof typeof FRAME_TYPE];
|
|
8
|
+
export declare const FRAME_HEADER_BYTES = 40;
|
|
9
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
10
|
+
export declare function frameFits(metaBytes: number, bodyBytes: number): boolean;
|
|
11
|
+
export interface Frame {
|
|
12
|
+
type: FrameType;
|
|
13
|
+
id: string;
|
|
14
|
+
peer: string;
|
|
15
|
+
meta: Uint8Array;
|
|
16
|
+
body: Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
export declare function isUuid(value: unknown): value is string;
|
|
19
|
+
export declare function uuidBytes(id: string): Uint8Array;
|
|
20
|
+
export declare function uuidString(bytes: Uint8Array): string;
|
|
21
|
+
export declare function parseFrame(data: Uint8Array): Frame | null;
|
|
22
|
+
export declare function encodeFrame(frame: Frame): Uint8Array;
|
|
23
|
+
export declare function encodeMeta(value: unknown): Uint8Array;
|
|
24
|
+
export declare function decodeMeta(meta: Uint8Array): unknown;
|
package/dist/frame.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const FRAME_VERSION = 1;
|
|
2
|
+
export const FRAME_TYPE = { request: 1, response: 2, routeError: 3 };
|
|
3
|
+
export const FRAME_HEADER_BYTES = 40;
|
|
4
|
+
export const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
5
|
+
export function frameFits(metaBytes, bodyBytes) {
|
|
6
|
+
return FRAME_HEADER_BYTES + metaBytes + bodyBytes <= MAX_FRAME_BYTES;
|
|
7
|
+
}
|
|
8
|
+
const PEER_OFFSET = 20;
|
|
9
|
+
const META_LENGTH_OFFSET = 36;
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
export function isUuid(value) {
|
|
12
|
+
return typeof value === "string" && UUID.test(value);
|
|
13
|
+
}
|
|
14
|
+
export function uuidBytes(id) {
|
|
15
|
+
if (!UUID.test(id))
|
|
16
|
+
throw new TypeError("UUID required");
|
|
17
|
+
return Buffer.from(id.replaceAll("-", ""), "hex");
|
|
18
|
+
}
|
|
19
|
+
export function uuidString(bytes) {
|
|
20
|
+
const hex = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("hex");
|
|
21
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
22
|
+
}
|
|
23
|
+
export function parseFrame(data) {
|
|
24
|
+
if (data.byteLength < FRAME_HEADER_BYTES)
|
|
25
|
+
return null;
|
|
26
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
27
|
+
const type = view.getUint8(1);
|
|
28
|
+
const metaEnd = FRAME_HEADER_BYTES + view.getUint32(META_LENGTH_OFFSET);
|
|
29
|
+
if (view.getUint8(0) !== FRAME_VERSION || view.getUint16(2) !== 0)
|
|
30
|
+
return null;
|
|
31
|
+
if (type < FRAME_TYPE.request || type > FRAME_TYPE.routeError || metaEnd > data.byteLength)
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
type: type,
|
|
35
|
+
id: uuidString(data.subarray(4, PEER_OFFSET)),
|
|
36
|
+
peer: uuidString(data.subarray(PEER_OFFSET, META_LENGTH_OFFSET)),
|
|
37
|
+
meta: data.subarray(FRAME_HEADER_BYTES, metaEnd),
|
|
38
|
+
body: data.subarray(metaEnd),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function encodeFrame(frame) {
|
|
42
|
+
const metaEnd = FRAME_HEADER_BYTES + frame.meta.byteLength;
|
|
43
|
+
const bytes = new Uint8Array(metaEnd + frame.body.byteLength);
|
|
44
|
+
const view = new DataView(bytes.buffer);
|
|
45
|
+
view.setUint8(0, FRAME_VERSION);
|
|
46
|
+
view.setUint8(1, frame.type);
|
|
47
|
+
view.setUint32(META_LENGTH_OFFSET, frame.meta.byteLength);
|
|
48
|
+
bytes.set(uuidBytes(frame.id), 4);
|
|
49
|
+
bytes.set(uuidBytes(frame.peer), PEER_OFFSET);
|
|
50
|
+
bytes.set(frame.meta, FRAME_HEADER_BYTES);
|
|
51
|
+
bytes.set(frame.body, metaEnd);
|
|
52
|
+
return bytes;
|
|
53
|
+
}
|
|
54
|
+
export function encodeMeta(value) {
|
|
55
|
+
return new TextEncoder().encode(JSON.stringify(value));
|
|
56
|
+
}
|
|
57
|
+
export function decodeMeta(meta) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(meta));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { connectHost, defaultDeviceName, type HostCredential, type HostEvent, type HostOptions, } from "./connect.js";
|
|
2
2
|
export { loginDevice, type DeviceSession } from "./login.js";
|
|
3
|
+
export { loginDeviceCode, type DeviceAuthorizationPrompt } from "./device-authorization.js";
|
|
3
4
|
export { createFileCredentialStore, type CredentialStore } from "./storage.js";
|
package/dist/host.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { connectGateway, type GatewayClientOptions, type ConnectionStatus, type ClientSocket, } from "./client.js";
|
|
2
2
|
export { MAX_MESSAGE_BYTES, byteLength, validId, type GatewayMessage, type InboundEnvelope, type OutboundEnvelope, } from "./protocol.js";
|
|
3
3
|
export { CloudError, cloudOrigin, cloudRequest, gatewayConfig } from "./cloud.js";
|
|
4
|
-
export { actionError, isRecord, parseResponse, type ActionResponse, type ActionRequest, } from "./actions.js";
|
|
4
|
+
export { actionError, isRecord, parseResponse, type ActionResponse, type ActionRequest, type BinaryActionResult, } from "./actions.js";
|
|
5
|
+
export { FRAME_TYPE, FRAME_VERSION, MAX_FRAME_BYTES, frameFits, decodeMeta, encodeFrame, encodeMeta, isUuid, parseFrame, uuidBytes, uuidString, type Frame, type FrameType, } from "./frame.js";
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { connectGateway, } from "./client.js";
|
|
|
2
2
|
export { MAX_MESSAGE_BYTES, byteLength, validId, } from "./protocol.js";
|
|
3
3
|
export { CloudError, cloudOrigin, cloudRequest, gatewayConfig } from "./cloud.js";
|
|
4
4
|
export { actionError, isRecord, parseResponse, } from "./actions.js";
|
|
5
|
+
export { FRAME_TYPE, FRAME_VERSION, MAX_FRAME_BYTES, frameFits, decodeMeta, encodeFrame, encodeMeta, isUuid, parseFrame, uuidBytes, uuidString, } from "./frame.js";
|
package/dist/login.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
token: string;
|
|
4
|
-
deviceId: string;
|
|
5
|
-
}
|
|
1
|
+
import { type DeviceSession } from "./device-session.js";
|
|
2
|
+
export type { DeviceSession } from "./device-session.js";
|
|
6
3
|
export declare function loginDevice(cloudUrl: string, name: string, signal: AbortSignal, onAuthorizationRequired: (url: string) => void): Promise<DeviceSession>;
|
package/dist/login.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { platform } from "node:os";
|
|
4
3
|
import { cloudOrigin } from "./cloud.js";
|
|
5
|
-
import {
|
|
4
|
+
import { devicePlatform, deviceSessionFromToken } from "./device-session.js";
|
|
6
5
|
export async function loginDevice(cloudUrl, name, signal, onAuthorizationRequired) {
|
|
7
6
|
const cloud = new URL(cloudOrigin(cloudUrl));
|
|
8
7
|
const verifier = randomBytes(32).toString("base64url");
|
|
@@ -36,20 +35,18 @@ export async function loginDevice(cloudUrl, name, signal, onAuthorizationRequire
|
|
|
36
35
|
redirect_uri: redirectUri,
|
|
37
36
|
code: incoming.searchParams.get("code"),
|
|
38
37
|
code_verifier: verifier,
|
|
39
|
-
platform:
|
|
38
|
+
platform: devicePlatform(),
|
|
40
39
|
}),
|
|
41
40
|
});
|
|
42
41
|
if (!result.ok)
|
|
43
42
|
throw new Error("Token exchange rejected");
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
!/^romedev_[A-Za-z0-9_-]{43}$/.test(body.access_token) ||
|
|
47
|
-
!validId(body.device_id))
|
|
43
|
+
const session = deviceSessionFromToken(await result.json(), cloud.origin);
|
|
44
|
+
if (!session)
|
|
48
45
|
throw new Error("Invalid token response");
|
|
49
46
|
response
|
|
50
47
|
.writeHead(200, { "content-type": "text/plain", "cache-control": "no-store" })
|
|
51
48
|
.end("Device authorized. Return to Rome Node.");
|
|
52
|
-
resolve(
|
|
49
|
+
resolve(session);
|
|
53
50
|
}
|
|
54
51
|
catch {
|
|
55
52
|
response
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type FileHandle } from "node:fs/promises";
|
|
2
|
+
import { type ActionResponse } from "./actions.js";
|
|
3
|
+
import type { CopyProgress, CopyRequest } from "./daemon-protocol.js";
|
|
4
|
+
export declare const TRANSFER_VERSION = 1;
|
|
5
|
+
export declare const TRANSFER_CHUNK_BYTES: number;
|
|
6
|
+
export declare const TRANSFER_WINDOW_BYTES: number;
|
|
7
|
+
export declare const TRANSFER_IDLE_MS = 60000;
|
|
8
|
+
export declare const MAX_HOST_TRANSFERS = 4;
|
|
9
|
+
export declare const PART_SUFFIX = ".rome-part";
|
|
10
|
+
export type TransferMessage = {
|
|
11
|
+
type: "transfer";
|
|
12
|
+
kind: "data";
|
|
13
|
+
offset: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: "transfer";
|
|
16
|
+
kind: "ack";
|
|
17
|
+
offset: number;
|
|
18
|
+
} | {
|
|
19
|
+
type: "transfer";
|
|
20
|
+
kind: "end";
|
|
21
|
+
size: number;
|
|
22
|
+
sha256: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: "transfer";
|
|
25
|
+
kind: "done";
|
|
26
|
+
size: number;
|
|
27
|
+
sha256: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: "transfer";
|
|
30
|
+
kind: "abort";
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function parseTransferMessage(value: unknown): TransferMessage | null;
|
|
35
|
+
export declare class TransferError extends Error {
|
|
36
|
+
readonly code: string;
|
|
37
|
+
constructor(code: string, message: string);
|
|
38
|
+
}
|
|
39
|
+
export type TransferSend = (meta: unknown, body?: Uint8Array) => boolean;
|
|
40
|
+
export interface TransferOptions {
|
|
41
|
+
chunkBytes?: number;
|
|
42
|
+
windowBytes?: number;
|
|
43
|
+
idleMs?: number;
|
|
44
|
+
onProgress?(bytes: number): void;
|
|
45
|
+
}
|
|
46
|
+
export interface TransferSummary {
|
|
47
|
+
size: number;
|
|
48
|
+
sha256: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TransferEndpoint {
|
|
51
|
+
readonly result: Promise<TransferSummary>;
|
|
52
|
+
receive(message: TransferMessage, body: Uint8Array): void;
|
|
53
|
+
abort(code: string, message: string, notify?: boolean): void;
|
|
54
|
+
}
|
|
55
|
+
export interface ReceiverEndpoint extends TransferEndpoint {
|
|
56
|
+
expect(size: number): void;
|
|
57
|
+
}
|
|
58
|
+
export interface Source {
|
|
59
|
+
size: number;
|
|
60
|
+
start(send: TransferSend, options?: TransferOptions): TransferEndpoint;
|
|
61
|
+
close(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export declare function openSource(path: string): Promise<Source>;
|
|
64
|
+
export interface Destination {
|
|
65
|
+
part: string;
|
|
66
|
+
start(send: TransferSend, options?: TransferOptions & {
|
|
67
|
+
size?: number;
|
|
68
|
+
}): ReceiverEndpoint;
|
|
69
|
+
cancel(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
export declare function createPartFile(path: string, id?: string): Promise<{
|
|
72
|
+
part: string;
|
|
73
|
+
handle: FileHandle;
|
|
74
|
+
}>;
|
|
75
|
+
export declare function openDestination(path: string): Promise<Destination>;
|
|
76
|
+
export declare class TransferHost {
|
|
77
|
+
private options;
|
|
78
|
+
private transfers;
|
|
79
|
+
constructor(options?: TransferOptions & {
|
|
80
|
+
limit?: number;
|
|
81
|
+
});
|
|
82
|
+
open(id: string, peer: string, args: unknown, send: TransferSend): Promise<void>;
|
|
83
|
+
receive(id: string, peer: string, message: TransferMessage, body: Uint8Array): void;
|
|
84
|
+
abortAll(): void;
|
|
85
|
+
}
|
|
86
|
+
export interface TransferChannel {
|
|
87
|
+
send: TransferSend;
|
|
88
|
+
close(): void;
|
|
89
|
+
}
|
|
90
|
+
export interface ChannelEvents {
|
|
91
|
+
frame(meta: unknown, body: Uint8Array): void;
|
|
92
|
+
lost(): void;
|
|
93
|
+
}
|
|
94
|
+
export interface CopyOptions extends Omit<TransferOptions, "onProgress"> {
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
onProgress?(progress: CopyProgress): void;
|
|
97
|
+
}
|
|
98
|
+
export declare function copyWithDevice(openChannel: (events: ChannelEvents) => Promise<TransferChannel | ActionResponse>, request: CopyRequest, options?: CopyOptions): Promise<{
|
|
99
|
+
bytes: number;
|
|
100
|
+
sha256: string;
|
|
101
|
+
ms: number;
|
|
102
|
+
}>;
|