relmio 0.3.1 → 0.4.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.
- package/CHANGELOG.md +32 -0
- package/README.md +67 -10
- package/docs/architecture.md +57 -0
- package/docs/local-endpoints-spec.md +370 -0
- package/docs/local-endpoints.md +374 -0
- package/docs/npm-publish.md +121 -201
- package/docs/security.md +92 -3
- package/package.json +2 -2
- package/src/domain/local-endpoints.js +460 -0
- package/src/gateway/openai.js +834 -0
- package/src/infrastructure/local-process.js +375 -0
- package/src/services/codex-login.js +711 -0
- package/src/services/local-installer.js +1120 -0
- package/src/ui/app.js +4 -0
- package/src/ui/index.html +13 -0
- package/src/ui/local.css +262 -0
- package/src/ui/local.html +442 -0
- package/src/ui/local.js +550 -0
- package/src/web/server.js +247 -12
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createLocalDockerEnvironment,
|
|
7
|
+
validateLocalDockerHost,
|
|
8
|
+
} from "../infrastructure/local-process.js";
|
|
9
|
+
|
|
10
|
+
const COMPOSE_FILE_NAME = "docker-compose.yml";
|
|
11
|
+
const PROJECT_NAME_PATTERN =
|
|
12
|
+
/^relmio-codex-chatgpt-[a-f0-9]{32}$/u;
|
|
13
|
+
const DEFAULT_RESPONSE_TIMEOUT_MS = 15_000;
|
|
14
|
+
const DEFAULT_COMPLETION_TIMEOUT_MS = 300_000;
|
|
15
|
+
const DEFAULT_TERMINATION_GRACE_MS = 2_000;
|
|
16
|
+
const DEFAULT_MAX_LINE_BYTES = 16 * 1024;
|
|
17
|
+
const DEFAULT_MAX_STDOUT_BYTES = 64 * 1024;
|
|
18
|
+
const DEFAULT_MAX_STDERR_BYTES = 32 * 1024;
|
|
19
|
+
const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { version: RELMIO_VERSION } = require("../../package.json");
|
|
22
|
+
|
|
23
|
+
const PROCESS_START_ERROR =
|
|
24
|
+
"The Codex sign-in process could not start. Check Docker and try again.";
|
|
25
|
+
const PROCESS_RESPONSE_ERROR =
|
|
26
|
+
"The Codex sign-in process returned an unexpected sign-in response.";
|
|
27
|
+
const PROCESS_OUTPUT_ERROR = "The Codex sign-in process returned too much data.";
|
|
28
|
+
const LOGIN_START_ERROR = "The Codex sign-in could not be started.";
|
|
29
|
+
const LOGIN_FAILED_ERROR = "The Codex sign-in was not completed.";
|
|
30
|
+
const LOGIN_MISMATCH_ERROR =
|
|
31
|
+
"The Codex sign-in process returned an unexpected sign-in response.";
|
|
32
|
+
const RESPONSE_TIMEOUT_ERROR =
|
|
33
|
+
"The Codex sign-in process timed out before providing a device code.";
|
|
34
|
+
const COMPLETION_TIMEOUT_ERROR =
|
|
35
|
+
"The Codex sign-in timed out before it was completed.";
|
|
36
|
+
const PROCESS_EARLY_CLOSE_ERROR =
|
|
37
|
+
"The Codex sign-in process ended before providing a device code.";
|
|
38
|
+
const PROCESS_INCOMPLETE_CLOSE_ERROR =
|
|
39
|
+
"The Codex sign-in process ended before sign-in completed.";
|
|
40
|
+
const LOGIN_CANCELLED_ERROR = "The Codex sign-in was cancelled.";
|
|
41
|
+
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasOwn(value, property) {
|
|
47
|
+
return Object.prototype.hasOwnProperty.call(value, property);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertPositiveInteger(value, label, maximum = Number.MAX_SAFE_INTEGER) {
|
|
51
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
|
52
|
+
throw new TypeError(`${label} must be a positive integer.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateOptions({
|
|
57
|
+
installDirectory,
|
|
58
|
+
dockerHost,
|
|
59
|
+
projectName,
|
|
60
|
+
responseTimeoutMs,
|
|
61
|
+
completionTimeoutMs,
|
|
62
|
+
terminationGraceMs,
|
|
63
|
+
maxLineBytes,
|
|
64
|
+
maxStdoutBytes,
|
|
65
|
+
maxStderrBytes,
|
|
66
|
+
}) {
|
|
67
|
+
if (
|
|
68
|
+
typeof installDirectory !== "string" ||
|
|
69
|
+
installDirectory.length === 0 ||
|
|
70
|
+
installDirectory.includes("\0") ||
|
|
71
|
+
!isAbsolute(installDirectory)
|
|
72
|
+
) {
|
|
73
|
+
throw new TypeError("The Codex install directory is invalid.");
|
|
74
|
+
}
|
|
75
|
+
const validatedDockerHost = validateLocalDockerHost(dockerHost);
|
|
76
|
+
if (
|
|
77
|
+
typeof projectName !== "string" ||
|
|
78
|
+
!PROJECT_NAME_PATTERN.test(projectName)
|
|
79
|
+
) {
|
|
80
|
+
throw new TypeError("The Codex Compose project identity is invalid.");
|
|
81
|
+
}
|
|
82
|
+
assertPositiveInteger(responseTimeoutMs, "responseTimeoutMs");
|
|
83
|
+
assertPositiveInteger(completionTimeoutMs, "completionTimeoutMs");
|
|
84
|
+
assertPositiveInteger(terminationGraceMs, "terminationGraceMs", 60_000);
|
|
85
|
+
assertPositiveInteger(maxLineBytes, "maxLineBytes");
|
|
86
|
+
assertPositiveInteger(maxStdoutBytes, "maxStdoutBytes");
|
|
87
|
+
assertPositiveInteger(maxStderrBytes, "maxStderrBytes");
|
|
88
|
+
if (maxLineBytes > maxStdoutBytes) {
|
|
89
|
+
throw new TypeError("maxLineBytes cannot exceed maxStdoutBytes.");
|
|
90
|
+
}
|
|
91
|
+
return { dockerHost: validatedDockerHost, projectName };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateVerificationUrl(value) {
|
|
95
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048) {
|
|
96
|
+
throw new TypeError();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let url;
|
|
100
|
+
try {
|
|
101
|
+
url = new URL(value);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new TypeError();
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
url.origin !== OPENAI_AUTH_ORIGIN ||
|
|
107
|
+
url.protocol !== "https:" ||
|
|
108
|
+
url.hostname !== "auth.openai.com" ||
|
|
109
|
+
url.port !== "" ||
|
|
110
|
+
url.username !== "" ||
|
|
111
|
+
url.password !== "" ||
|
|
112
|
+
url.hash !== ""
|
|
113
|
+
) {
|
|
114
|
+
throw new TypeError();
|
|
115
|
+
}
|
|
116
|
+
return url.toString();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateDeviceResponse(value) {
|
|
120
|
+
if (
|
|
121
|
+
!isPlainObject(value) ||
|
|
122
|
+
value.type !== "chatgptDeviceCode" ||
|
|
123
|
+
typeof value.loginId !== "string" ||
|
|
124
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(value.loginId) ||
|
|
125
|
+
typeof value.userCode !== "string" ||
|
|
126
|
+
value.userCode.length < 4 ||
|
|
127
|
+
value.userCode.length > 32 ||
|
|
128
|
+
!/^[A-Z0-9]+(?:-[A-Z0-9]+)*$/u.test(value.userCode)
|
|
129
|
+
) {
|
|
130
|
+
throw new TypeError();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
loginId: value.loginId,
|
|
135
|
+
userCode: value.userCode,
|
|
136
|
+
verificationUrl: validateVerificationUrl(value.verificationUrl),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createInitializeMessage() {
|
|
141
|
+
return {
|
|
142
|
+
id: 0,
|
|
143
|
+
method: "initialize",
|
|
144
|
+
params: {
|
|
145
|
+
clientInfo: {
|
|
146
|
+
name: "relmio",
|
|
147
|
+
title: "Relmio",
|
|
148
|
+
version: RELMIO_VERSION,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createPostInitializeMessages() {
|
|
155
|
+
return [
|
|
156
|
+
{ method: "initialized", params: {} },
|
|
157
|
+
{
|
|
158
|
+
id: 1,
|
|
159
|
+
method: "account/login/start",
|
|
160
|
+
params: { type: "chatgptDeviceCode" },
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function startCodexDeviceLogin({
|
|
166
|
+
installDirectory,
|
|
167
|
+
dockerHost,
|
|
168
|
+
projectName,
|
|
169
|
+
spawnProcess = spawn,
|
|
170
|
+
responseTimeoutMs = DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
171
|
+
completionTimeoutMs = DEFAULT_COMPLETION_TIMEOUT_MS,
|
|
172
|
+
terminationGraceMs = DEFAULT_TERMINATION_GRACE_MS,
|
|
173
|
+
maxLineBytes = DEFAULT_MAX_LINE_BYTES,
|
|
174
|
+
maxStdoutBytes = DEFAULT_MAX_STDOUT_BYTES,
|
|
175
|
+
maxStderrBytes = DEFAULT_MAX_STDERR_BYTES,
|
|
176
|
+
setTimer = setTimeout,
|
|
177
|
+
clearTimer = clearTimeout,
|
|
178
|
+
environment = process.env,
|
|
179
|
+
} = {}) {
|
|
180
|
+
const validated = validateOptions({
|
|
181
|
+
installDirectory,
|
|
182
|
+
dockerHost,
|
|
183
|
+
projectName,
|
|
184
|
+
responseTimeoutMs,
|
|
185
|
+
completionTimeoutMs,
|
|
186
|
+
terminationGraceMs,
|
|
187
|
+
maxLineBytes,
|
|
188
|
+
maxStdoutBytes,
|
|
189
|
+
maxStderrBytes,
|
|
190
|
+
});
|
|
191
|
+
const childEnvironment = createLocalDockerEnvironment(environment);
|
|
192
|
+
|
|
193
|
+
let child;
|
|
194
|
+
try {
|
|
195
|
+
child = spawnProcess(
|
|
196
|
+
"docker",
|
|
197
|
+
[
|
|
198
|
+
"--host",
|
|
199
|
+
validated.dockerHost,
|
|
200
|
+
"compose",
|
|
201
|
+
"--project-name",
|
|
202
|
+
validated.projectName,
|
|
203
|
+
"--file",
|
|
204
|
+
COMPOSE_FILE_NAME,
|
|
205
|
+
"run",
|
|
206
|
+
"--rm",
|
|
207
|
+
"--no-deps",
|
|
208
|
+
"codex",
|
|
209
|
+
"app-server",
|
|
210
|
+
"--strict-config",
|
|
211
|
+
"--stdio",
|
|
212
|
+
],
|
|
213
|
+
{
|
|
214
|
+
cwd: installDirectory,
|
|
215
|
+
env: childEnvironment,
|
|
216
|
+
shell: false,
|
|
217
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
218
|
+
windowsHide: true,
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error(PROCESS_START_ERROR);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let resolveDeviceResponse;
|
|
226
|
+
let rejectDeviceResponse;
|
|
227
|
+
let deviceResponseSettled = false;
|
|
228
|
+
let expectedLoginId = null;
|
|
229
|
+
const deviceResponsePromise = new Promise((resolvePromise, rejectPromise) => {
|
|
230
|
+
resolveDeviceResponse = resolvePromise;
|
|
231
|
+
rejectDeviceResponse = rejectPromise;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let resolveCompletion;
|
|
235
|
+
let rejectCompletion;
|
|
236
|
+
let completionSettled = false;
|
|
237
|
+
const completion = new Promise((resolvePromise, rejectPromise) => {
|
|
238
|
+
resolveCompletion = resolvePromise;
|
|
239
|
+
rejectCompletion = rejectPromise;
|
|
240
|
+
});
|
|
241
|
+
// Startup failures can reject before completion is returned to the caller.
|
|
242
|
+
void completion.catch(() => {});
|
|
243
|
+
|
|
244
|
+
let responseTimer;
|
|
245
|
+
let completionTimer;
|
|
246
|
+
let killTimer;
|
|
247
|
+
let forceSettleTimer;
|
|
248
|
+
let successCloseTimer;
|
|
249
|
+
let processClosed = false;
|
|
250
|
+
let terminationRequested = false;
|
|
251
|
+
let pendingFailure = null;
|
|
252
|
+
let pendingSuccess = false;
|
|
253
|
+
let protocolPhase = "waitingInitialize";
|
|
254
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
255
|
+
let stdoutBytes = 0;
|
|
256
|
+
let stderrBytes = 0;
|
|
257
|
+
|
|
258
|
+
const clearScheduledTimer = (timer) => {
|
|
259
|
+
if (timer === undefined) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
clearTimer(timer);
|
|
264
|
+
} catch {
|
|
265
|
+
// Timer cleanup must not replace the selected redacted result.
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const clearResponseTimer = () => {
|
|
270
|
+
clearScheduledTimer(responseTimer);
|
|
271
|
+
responseTimer = undefined;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const clearCompletionTimer = () => {
|
|
275
|
+
clearScheduledTimer(completionTimer);
|
|
276
|
+
completionTimer = undefined;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const clearKillTimer = () => {
|
|
280
|
+
clearScheduledTimer(killTimer);
|
|
281
|
+
killTimer = undefined;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const clearForceSettleTimer = () => {
|
|
285
|
+
clearScheduledTimer(forceSettleTimer);
|
|
286
|
+
forceSettleTimer = undefined;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const clearSuccessCloseTimer = () => {
|
|
290
|
+
clearScheduledTimer(successCloseTimer);
|
|
291
|
+
successCloseTimer = undefined;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const signalChild = (signal) => {
|
|
295
|
+
try {
|
|
296
|
+
child.kill(signal);
|
|
297
|
+
} catch {
|
|
298
|
+
// Never expose platform- or process-specific termination details.
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const requestTermination = () => {
|
|
303
|
+
if (terminationRequested || processClosed) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
terminationRequested = true;
|
|
307
|
+
signalChild("SIGTERM");
|
|
308
|
+
if (processClosed) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
killTimer = setTimer(() => {
|
|
313
|
+
killTimer = undefined;
|
|
314
|
+
if (!processClosed) {
|
|
315
|
+
signalChild("SIGKILL");
|
|
316
|
+
try {
|
|
317
|
+
forceSettleTimer = setTimer(() => {
|
|
318
|
+
forceSettleTimer = undefined;
|
|
319
|
+
if (!processClosed) {
|
|
320
|
+
if (pendingFailure) {
|
|
321
|
+
settleFailureNow(pendingFailure);
|
|
322
|
+
} else if (pendingSuccess) {
|
|
323
|
+
settleSuccessNow();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}, terminationGraceMs);
|
|
327
|
+
} catch {
|
|
328
|
+
if (pendingFailure) {
|
|
329
|
+
settleFailureNow(pendingFailure);
|
|
330
|
+
} else if (pendingSuccess) {
|
|
331
|
+
settleSuccessNow();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}, terminationGraceMs);
|
|
336
|
+
} catch {
|
|
337
|
+
signalChild("SIGKILL");
|
|
338
|
+
if (pendingFailure) {
|
|
339
|
+
settleFailureNow(pendingFailure);
|
|
340
|
+
} else if (pendingSuccess) {
|
|
341
|
+
settleSuccessNow();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const settleFailureNow = (message) => {
|
|
347
|
+
clearResponseTimer();
|
|
348
|
+
clearCompletionTimer();
|
|
349
|
+
clearKillTimer();
|
|
350
|
+
clearForceSettleTimer();
|
|
351
|
+
clearSuccessCloseTimer();
|
|
352
|
+
pendingFailure = null;
|
|
353
|
+
pendingSuccess = false;
|
|
354
|
+
if (!deviceResponseSettled) {
|
|
355
|
+
deviceResponseSettled = true;
|
|
356
|
+
rejectDeviceResponse(new Error(message));
|
|
357
|
+
}
|
|
358
|
+
if (!completionSettled) {
|
|
359
|
+
completionSettled = true;
|
|
360
|
+
rejectCompletion(new Error(message));
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const settleSuccessNow = () => {
|
|
365
|
+
clearResponseTimer();
|
|
366
|
+
clearCompletionTimer();
|
|
367
|
+
clearKillTimer();
|
|
368
|
+
clearForceSettleTimer();
|
|
369
|
+
clearSuccessCloseTimer();
|
|
370
|
+
pendingSuccess = false;
|
|
371
|
+
if (!completionSettled) {
|
|
372
|
+
completionSettled = true;
|
|
373
|
+
resolveCompletion({ success: true });
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const requestFailure = (message, { terminateProcess = true } = {}) => {
|
|
378
|
+
if (
|
|
379
|
+
pendingFailure ||
|
|
380
|
+
pendingSuccess ||
|
|
381
|
+
(deviceResponseSettled && completionSettled)
|
|
382
|
+
) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
protocolPhase = "terminating";
|
|
386
|
+
clearResponseTimer();
|
|
387
|
+
clearCompletionTimer();
|
|
388
|
+
if (!terminateProcess || processClosed) {
|
|
389
|
+
settleFailureNow(message);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
pendingFailure = message;
|
|
393
|
+
requestTermination();
|
|
394
|
+
return true;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const settleProtocolFailure = (message = PROCESS_RESPONSE_ERROR) => {
|
|
398
|
+
requestFailure(message);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const writeMessage = (message) => {
|
|
402
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const acceptInitialize = (message) => {
|
|
406
|
+
if (protocolPhase !== "waitingInitialize") {
|
|
407
|
+
settleProtocolFailure(LOGIN_MISMATCH_ERROR);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (hasOwn(message, "error") || !isPlainObject(message.result)) {
|
|
411
|
+
settleProtocolFailure(LOGIN_START_ERROR);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
protocolPhase = "waitingLogin";
|
|
416
|
+
try {
|
|
417
|
+
for (const nextMessage of createPostInitializeMessages()) {
|
|
418
|
+
writeMessage(nextMessage);
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
requestFailure(PROCESS_START_ERROR);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const acceptDeviceResponse = (result) => {
|
|
426
|
+
if (protocolPhase !== "waitingLogin" || deviceResponseSettled) {
|
|
427
|
+
settleProtocolFailure(LOGIN_MISMATCH_ERROR);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let device;
|
|
432
|
+
try {
|
|
433
|
+
device = validateDeviceResponse(result);
|
|
434
|
+
} catch {
|
|
435
|
+
settleProtocolFailure(PROCESS_RESPONSE_ERROR);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
expectedLoginId = device.loginId;
|
|
440
|
+
protocolPhase = "waitingCompletion";
|
|
441
|
+
clearResponseTimer();
|
|
442
|
+
try {
|
|
443
|
+
completionTimer = setTimer(
|
|
444
|
+
() => {
|
|
445
|
+
completionTimer = undefined;
|
|
446
|
+
requestFailure(COMPLETION_TIMEOUT_ERROR);
|
|
447
|
+
},
|
|
448
|
+
completionTimeoutMs,
|
|
449
|
+
);
|
|
450
|
+
} catch {
|
|
451
|
+
requestFailure(PROCESS_START_ERROR);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
deviceResponseSettled = true;
|
|
455
|
+
resolveDeviceResponse({
|
|
456
|
+
verificationUrl: device.verificationUrl,
|
|
457
|
+
userCode: device.userCode,
|
|
458
|
+
completion,
|
|
459
|
+
cancel() {
|
|
460
|
+
requestFailure(LOGIN_CANCELLED_ERROR);
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const acceptCompletion = (params) => {
|
|
466
|
+
if (
|
|
467
|
+
protocolPhase !== "waitingCompletion" ||
|
|
468
|
+
!isPlainObject(params) ||
|
|
469
|
+
expectedLoginId === null ||
|
|
470
|
+
typeof params.loginId !== "string" ||
|
|
471
|
+
params.loginId !== expectedLoginId ||
|
|
472
|
+
typeof params.success !== "boolean" ||
|
|
473
|
+
!(
|
|
474
|
+
params.error === undefined ||
|
|
475
|
+
params.error === null ||
|
|
476
|
+
typeof params.error === "string"
|
|
477
|
+
)
|
|
478
|
+
) {
|
|
479
|
+
settleProtocolFailure(LOGIN_MISMATCH_ERROR);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (params.success !== true) {
|
|
483
|
+
requestFailure(LOGIN_FAILED_ERROR);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (completionSettled || pendingSuccess) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
protocolPhase = "completing";
|
|
491
|
+
pendingSuccess = true;
|
|
492
|
+
clearCompletionTimer();
|
|
493
|
+
try {
|
|
494
|
+
child.stdin.end();
|
|
495
|
+
} catch {
|
|
496
|
+
// Completion is established and no sensitive detail is useful.
|
|
497
|
+
}
|
|
498
|
+
if (processClosed) {
|
|
499
|
+
settleSuccessNow();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
successCloseTimer = setTimer(() => {
|
|
504
|
+
successCloseTimer = undefined;
|
|
505
|
+
requestTermination();
|
|
506
|
+
}, terminationGraceMs);
|
|
507
|
+
} catch {
|
|
508
|
+
requestTermination();
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const processLine = (lineBuffer) => {
|
|
513
|
+
if (pendingFailure || pendingSuccess || completionSettled) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (lineBuffer.length > maxLineBytes) {
|
|
517
|
+
requestFailure(PROCESS_OUTPUT_ERROR);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const withoutCarriageReturn =
|
|
521
|
+
lineBuffer.at(-1) === 0x0d ? lineBuffer.subarray(0, -1) : lineBuffer;
|
|
522
|
+
if (withoutCarriageReturn.length === 0) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
let message;
|
|
527
|
+
try {
|
|
528
|
+
message = JSON.parse(withoutCarriageReturn.toString("utf8"));
|
|
529
|
+
} catch {
|
|
530
|
+
settleProtocolFailure(PROCESS_RESPONSE_ERROR);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!isPlainObject(message)) {
|
|
534
|
+
settleProtocolFailure(PROCESS_RESPONSE_ERROR);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (hasOwn(message, "id")) {
|
|
539
|
+
if (message.id === 0) {
|
|
540
|
+
acceptInitialize(message);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (message.id === 1) {
|
|
544
|
+
if (protocolPhase !== "waitingLogin") {
|
|
545
|
+
settleProtocolFailure(LOGIN_MISMATCH_ERROR);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (hasOwn(message, "error")) {
|
|
549
|
+
settleProtocolFailure(LOGIN_START_ERROR);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (!hasOwn(message, "result")) {
|
|
553
|
+
settleProtocolFailure(PROCESS_RESPONSE_ERROR);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
acceptDeviceResponse(message.result);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
settleProtocolFailure(PROCESS_RESPONSE_ERROR);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (message.method === "account/login/completed") {
|
|
563
|
+
acceptCompletion(message.params);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const consumeStdout = (chunk) => {
|
|
568
|
+
if (pendingFailure || pendingSuccess || completionSettled) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
let bytes;
|
|
572
|
+
try {
|
|
573
|
+
bytes = Buffer.from(chunk);
|
|
574
|
+
} catch {
|
|
575
|
+
settleProtocolFailure(PROCESS_RESPONSE_ERROR);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
stdoutBytes += bytes.length;
|
|
579
|
+
if (stdoutBytes > maxStdoutBytes) {
|
|
580
|
+
requestFailure(PROCESS_OUTPUT_ERROR);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, bytes]);
|
|
584
|
+
|
|
585
|
+
let newlineIndex = stdoutBuffer.indexOf(0x0a);
|
|
586
|
+
while (
|
|
587
|
+
newlineIndex >= 0 &&
|
|
588
|
+
!pendingFailure &&
|
|
589
|
+
!pendingSuccess &&
|
|
590
|
+
!completionSettled
|
|
591
|
+
) {
|
|
592
|
+
const line = stdoutBuffer.subarray(0, newlineIndex);
|
|
593
|
+
stdoutBuffer = stdoutBuffer.subarray(newlineIndex + 1);
|
|
594
|
+
processLine(line);
|
|
595
|
+
newlineIndex = stdoutBuffer.indexOf(0x0a);
|
|
596
|
+
}
|
|
597
|
+
if (
|
|
598
|
+
!pendingFailure &&
|
|
599
|
+
!pendingSuccess &&
|
|
600
|
+
!completionSettled &&
|
|
601
|
+
stdoutBuffer.length > maxLineBytes
|
|
602
|
+
) {
|
|
603
|
+
requestFailure(PROCESS_OUTPUT_ERROR);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const consumeStderr = (chunk) => {
|
|
608
|
+
if (pendingFailure || pendingSuccess || completionSettled) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
let byteLength;
|
|
612
|
+
try {
|
|
613
|
+
byteLength = Buffer.byteLength(chunk);
|
|
614
|
+
} catch {
|
|
615
|
+
settleProtocolFailure(PROCESS_RESPONSE_ERROR);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
stderrBytes += byteLength;
|
|
619
|
+
if (stderrBytes > maxStderrBytes) {
|
|
620
|
+
requestFailure(PROCESS_OUTPUT_ERROR);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
if (
|
|
625
|
+
!child ||
|
|
626
|
+
typeof child.once !== "function" ||
|
|
627
|
+
typeof child.kill !== "function" ||
|
|
628
|
+
typeof child.stdout?.on !== "function" ||
|
|
629
|
+
typeof child.stderr?.on !== "function" ||
|
|
630
|
+
typeof child.stdin?.write !== "function" ||
|
|
631
|
+
typeof child.stdin?.end !== "function"
|
|
632
|
+
) {
|
|
633
|
+
settleFailureNow(PROCESS_START_ERROR);
|
|
634
|
+
return deviceResponsePromise;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
child.stdout.on("data", consumeStdout);
|
|
639
|
+
child.stderr.on("data", consumeStderr);
|
|
640
|
+
child.stdin.on?.("error", () => {
|
|
641
|
+
if (pendingSuccess) {
|
|
642
|
+
clearSuccessCloseTimer();
|
|
643
|
+
requestTermination();
|
|
644
|
+
} else if (!pendingFailure) {
|
|
645
|
+
requestFailure(
|
|
646
|
+
deviceResponseSettled
|
|
647
|
+
? PROCESS_INCOMPLETE_CLOSE_ERROR
|
|
648
|
+
: PROCESS_START_ERROR,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
child.once("error", () => {
|
|
653
|
+
if (pendingSuccess) {
|
|
654
|
+
clearSuccessCloseTimer();
|
|
655
|
+
requestTermination();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (pendingFailure) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
requestFailure(
|
|
662
|
+
deviceResponseSettled
|
|
663
|
+
? PROCESS_INCOMPLETE_CLOSE_ERROR
|
|
664
|
+
: PROCESS_START_ERROR,
|
|
665
|
+
);
|
|
666
|
+
});
|
|
667
|
+
child.once("close", () => {
|
|
668
|
+
processClosed = true;
|
|
669
|
+
clearKillTimer();
|
|
670
|
+
clearForceSettleTimer();
|
|
671
|
+
clearSuccessCloseTimer();
|
|
672
|
+
if (pendingFailure) {
|
|
673
|
+
settleFailureNow(pendingFailure);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (!pendingSuccess && !completionSettled && stdoutBuffer.length > 0) {
|
|
677
|
+
const finalLine = stdoutBuffer;
|
|
678
|
+
stdoutBuffer = Buffer.alloc(0);
|
|
679
|
+
processLine(finalLine);
|
|
680
|
+
}
|
|
681
|
+
if (pendingFailure) {
|
|
682
|
+
settleFailureNow(pendingFailure);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (pendingSuccess) {
|
|
686
|
+
settleSuccessNow();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (deviceResponseSettled && completionSettled) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (!deviceResponseSettled) {
|
|
693
|
+
requestFailure(PROCESS_EARLY_CLOSE_ERROR, { terminateProcess: false });
|
|
694
|
+
} else if (!completionSettled) {
|
|
695
|
+
requestFailure(PROCESS_INCOMPLETE_CLOSE_ERROR, {
|
|
696
|
+
terminateProcess: false,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
responseTimer = setTimer(() => {
|
|
702
|
+
responseTimer = undefined;
|
|
703
|
+
requestFailure(RESPONSE_TIMEOUT_ERROR);
|
|
704
|
+
}, responseTimeoutMs);
|
|
705
|
+
writeMessage(createInitializeMessage());
|
|
706
|
+
} catch {
|
|
707
|
+
requestFailure(PROCESS_START_ERROR);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return deviceResponsePromise;
|
|
711
|
+
}
|