@rome-os/node 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/dist/daemon.js DELETED
@@ -1,208 +0,0 @@
1
- import { randomBytes, timingSafeEqual } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
- import { createServer } from "node:http";
4
- import { setTimeout as delay } from "node:timers/promises";
5
- import { actionError, isRecord } from "./actions.js";
6
- import { DeviceConnector } from "./connector.js";
7
- import { configRoot, daemonPort, daemonStatePath, readCallerCredential } from "./local.js";
8
- import { MAX_MESSAGE_BYTES } from "./protocol.js";
9
- import { readPrivateJson, writePrivateJson } from "./storage.js";
10
- async function readDaemonState() {
11
- const value = await readPrivateJson(daemonStatePath());
12
- if (!isRecord(value) ||
13
- typeof value.token !== "string" ||
14
- !/^romenode_[A-Za-z0-9_-]{43}$/.test(value.token) ||
15
- value.port !== daemonPort() ||
16
- typeof value.pid !== "number" ||
17
- !Number.isSafeInteger(value.pid) ||
18
- value.pid <= 0)
19
- return null;
20
- return { token: value.token, port: daemonPort(), pid: value.pid };
21
- }
22
- async function request(state, path, body) {
23
- return fetch(`http://127.0.0.1:${state.port}${path}`, {
24
- method: body === undefined ? "GET" : "POST",
25
- redirect: "error",
26
- signal: AbortSignal.timeout(path === "/status" ? 1000 : 85_000),
27
- headers: { authorization: `Bearer ${state.token}`, "content-type": "application/json" },
28
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
29
- });
30
- }
31
- export async function daemonStatus() {
32
- const state = await readDaemonState();
33
- if (!state)
34
- return null;
35
- try {
36
- const response = await request(state, "/status");
37
- const value = await response.json();
38
- return response.ok && isRecord(value) && value.pid === state.pid ? state : null;
39
- }
40
- catch {
41
- return null;
42
- }
43
- }
44
- export async function startDaemon() {
45
- const current = await daemonStatus();
46
- if (current)
47
- return current;
48
- await readCallerCredential();
49
- const child = spawn(process.execPath, [process.argv[1], "daemon", "serve"], {
50
- detached: true,
51
- stdio: "ignore",
52
- windowsHide: true,
53
- env: { ...process.env, ROME_NODE_CONFIG_DIR: configRoot() },
54
- });
55
- let failed = false;
56
- child.on("error", () => {
57
- failed = true;
58
- });
59
- child.unref();
60
- const deadline = Date.now() + 5000;
61
- while (Date.now() < deadline && !failed) {
62
- const state = await daemonStatus();
63
- if (state)
64
- return state;
65
- await delay(50);
66
- }
67
- throw new Error("Could not start the CLI daemon. Run rome-node daemon serve to inspect the error. If its port is occupied, set ROME_NODE_DAEMON_PORT.");
68
- }
69
- export async function stopDaemon() {
70
- const state = await daemonStatus();
71
- if (!state)
72
- return;
73
- const response = await request(state, "/stop", {});
74
- if (!response.ok)
75
- throw new Error("Could not stop the CLI daemon.");
76
- const deadline = Date.now() + 5000;
77
- while (Date.now() < deadline) {
78
- try {
79
- const status = await request(state, "/status");
80
- if (status.status === 401)
81
- return;
82
- }
83
- catch {
84
- return;
85
- }
86
- await delay(50);
87
- }
88
- throw new Error("The CLI daemon has not stopped yet.");
89
- }
90
- export async function callDaemon(path, body) {
91
- const state = await startDaemon();
92
- try {
93
- return await request(state, path, body);
94
- }
95
- catch {
96
- throw new Error("CLI daemon connection failed. Execution outcome may be unknown; do not automatically retry.");
97
- }
98
- }
99
- function json(response, status, value) {
100
- response.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
101
- response.end(JSON.stringify(value));
102
- }
103
- async function readBody(request) {
104
- let size = 0;
105
- const chunks = [];
106
- for await (const chunk of request) {
107
- const buffer = Buffer.from(chunk);
108
- size += buffer.length;
109
- if (size > MAX_MESSAGE_BYTES)
110
- throw new Error("message_too_large");
111
- chunks.push(buffer);
112
- }
113
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
114
- }
115
- export async function serveDaemon() {
116
- const credential = await readCallerCredential();
117
- const connector = new DeviceConnector({ credential });
118
- const state = {
119
- port: daemonPort(),
120
- pid: process.pid,
121
- token: `romenode_${randomBytes(32).toString("base64url")}`,
122
- };
123
- let closing = false;
124
- const server = createServer((req, res) => {
125
- void handle(req, res).catch(() => {
126
- if (!res.headersSent)
127
- json(res, 503, actionError("gateway_unavailable", "The device service is unavailable."));
128
- else
129
- res.destroy();
130
- });
131
- });
132
- server.requestTimeout = 10_000;
133
- server.headersTimeout = 10_000;
134
- const close = () => {
135
- if (closing)
136
- return;
137
- closing = true;
138
- connector.stop();
139
- server.close();
140
- server.closeIdleConnections();
141
- const timer = setTimeout(() => server.closeAllConnections(), 1000);
142
- timer.unref();
143
- };
144
- async function handle(req, res) {
145
- const actual = Buffer.from(req.headers.authorization ?? "");
146
- const expected = Buffer.from(`Bearer ${state.token}`);
147
- if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
148
- json(res, 401, actionError("unauthorized", "A CLI daemon credential is required."));
149
- return;
150
- }
151
- if (closing) {
152
- json(res, 503, actionError("daemon_stopped", "The CLI daemon is stopping."));
153
- }
154
- else if (req.method === "GET" && req.url === "/status") {
155
- json(res, 200, { pid: state.pid });
156
- }
157
- else if (req.method === "POST" && req.url === "/stop") {
158
- json(res, 200, { stopped: true });
159
- close();
160
- }
161
- else if (req.method === "GET" && req.url === "/devices") {
162
- json(res, 200, await connector.list());
163
- }
164
- else if (req.method === "POST" && req.url === "/run") {
165
- if (Number(req.headers["content-length"]) > MAX_MESSAGE_BYTES) {
166
- json(res, 413, actionError("message_too_large", "The request exceeds 128 KiB."));
167
- req.resume();
168
- return;
169
- }
170
- let body;
171
- try {
172
- body = await readBody(req);
173
- }
174
- catch (error) {
175
- const tooLarge = error instanceof Error && error.message === "message_too_large";
176
- json(res, tooLarge ? 413 : 400, actionError(tooLarge ? "message_too_large" : "invalid_request", "Invalid device request."));
177
- return;
178
- }
179
- if (!isRecord(body) || typeof body.deviceId !== "string" || typeof body.action !== "string") {
180
- json(res, 400, actionError("invalid_request", "A device ID and action are required."));
181
- return;
182
- }
183
- json(res, 200, await connector.run(body.deviceId, body.action, body.args ?? {}));
184
- }
185
- else {
186
- json(res, 404, actionError("not_found", "Unknown CLI daemon operation."));
187
- }
188
- }
189
- await new Promise((resolve, reject) => {
190
- server.once("error", reject);
191
- server.listen(state.port, "127.0.0.1", () => {
192
- server.removeListener("error", reject);
193
- resolve();
194
- });
195
- });
196
- const finished = new Promise((resolve) => server.once("close", resolve));
197
- process.once("SIGINT", close);
198
- process.once("SIGTERM", close);
199
- try {
200
- await writePrivateJson(daemonStatePath(), state);
201
- await finished;
202
- }
203
- finally {
204
- close();
205
- process.removeListener("SIGINT", close);
206
- process.removeListener("SIGTERM", close);
207
- }
208
- }
@@ -1,5 +0,0 @@
1
- import { type InboundEnvelope, type OutboundEnvelope } from "./protocol.js";
2
- export declare function createExecutor(name: string, send: (message: OutboundEnvelope) => boolean, secrets?: string[]): {
3
- receive(message: InboundEnvelope): Promise<void>;
4
- disconnect(): void;
5
- };
package/dist/executor.js DELETED
@@ -1,121 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { platform } from "node:os";
3
- import { actionError, isRecord } from "./actions.js";
4
- import { byteLength, MAX_MESSAGE_BYTES, } from "./protocol.js";
5
- export function createExecutor(name, send, secrets = []) {
6
- const children = new Set();
7
- let generation = 0;
8
- const handlers = {
9
- "system.info": async () => ({
10
- type: "response",
11
- ok: true,
12
- result: {
13
- name,
14
- platform: platform() === "darwin" ? "macos" : platform() === "win32" ? "windows" : platform(),
15
- actions: ["system.info", "exec"],
16
- },
17
- }),
18
- exec: async (input) => {
19
- if (!isRecord(input) ||
20
- typeof input.command !== "string" ||
21
- !input.command ||
22
- input.command.includes("\0") ||
23
- (input.args !== undefined &&
24
- (!Array.isArray(input.args) ||
25
- !input.args.every((arg) => typeof arg === "string" && !arg.includes("\0")))) ||
26
- (input.cwd !== undefined && (typeof input.cwd !== "string" || input.cwd.includes("\0"))))
27
- return actionError("invalid_args", "exec requires command, optional string args[], and optional cwd.");
28
- if (children.size >= 8)
29
- return actionError("busy", "The computer is already running eight programs.");
30
- const command = input.command;
31
- const args = (input.args ?? []);
32
- const cwd = input.cwd;
33
- return new Promise((resolve) => {
34
- const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(?:ROME_.*(?:TOKEN|SECRET|CREDENTIAL)|NODE_OPTIONS)$/i.test(key)));
35
- let child;
36
- try {
37
- child = spawn(command, args, {
38
- cwd,
39
- env: environment,
40
- shell: false,
41
- windowsHide: true,
42
- stdio: ["ignore", "pipe", "pipe"],
43
- });
44
- }
45
- catch {
46
- resolve(actionError("exec_failed", "Could not start the program."));
47
- return;
48
- }
49
- children.add(child);
50
- const output = { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
51
- const truncated = { stdout: false, stderr: false };
52
- for (const stream of ["stdout", "stderr"]) {
53
- child[stream]?.on("data", (chunk) => {
54
- const remaining = 48 * 1024 - output[stream].length;
55
- if (chunk.length > remaining)
56
- truncated[stream] = true;
57
- output[stream] = Buffer.concat([output[stream], chunk.subarray(0, remaining)]);
58
- });
59
- }
60
- child.once("error", () => {
61
- children.delete(child);
62
- resolve(actionError("exec_failed", "Could not start the program. Check the executable and working directory."));
63
- });
64
- child.once("close", (exitCode, signal) => {
65
- children.delete(child);
66
- const redact = (value) => secrets.reduce((text, secret) => text.replaceAll(secret, "[redacted]"), value.toString("utf8"));
67
- resolve({
68
- type: "response",
69
- ok: true,
70
- result: {
71
- exitCode,
72
- signal,
73
- stdout: redact(output.stdout),
74
- stderr: redact(output.stderr),
75
- truncated,
76
- },
77
- });
78
- });
79
- });
80
- },
81
- };
82
- return {
83
- async receive(message) {
84
- const request = message.payload;
85
- if (!isRecord(request) || request.type !== "request" || typeof request.action !== "string")
86
- return;
87
- const current = generation;
88
- const handler = Object.hasOwn(handlers, request.action)
89
- ? handlers[request.action]
90
- : undefined;
91
- const payload = handler
92
- ? await handler(request.args).catch(() => actionError("exec_failed", "The action failed."))
93
- : actionError("unsupported_action", "The computer does not support this action.");
94
- const reply = { id: message.id, to: message.from, payload };
95
- if (payload.ok && isRecord(payload.result) && isRecord(payload.result.truncated)) {
96
- const result = payload.result;
97
- while (byteLength(JSON.stringify(reply)) > MAX_MESSAGE_BYTES) {
98
- for (const stream of ["stdout", "stderr"]) {
99
- if (typeof result[stream] === "string" && result[stream].length > 0) {
100
- result[stream] = result[stream].slice(0, Math.floor(result[stream].length * 0.75));
101
- result.truncated[stream] = true;
102
- }
103
- }
104
- }
105
- }
106
- if (current === generation)
107
- send(reply);
108
- },
109
- disconnect() {
110
- generation++;
111
- for (const child of children) {
112
- child.kill("SIGTERM");
113
- const timer = setTimeout(() => {
114
- if (child.exitCode === null && child.signalCode === null)
115
- child.kill("SIGKILL");
116
- }, 1000);
117
- timer.unref();
118
- }
119
- },
120
- };
121
- }
package/dist/local.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export declare function configRoot(): string;
2
- export declare const callerCredentialPath: () => string;
3
- export declare const daemonStatePath: () => string;
4
- export declare function daemonPort(): number;
5
- export interface CallerCredential {
6
- cloudUrl: string;
7
- token: string;
8
- }
9
- export declare function readCallerCredential(): Promise<CallerCredential>;
package/dist/local.js DELETED
@@ -1,33 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { homedir } from "node:os";
3
- import { join, resolve } from "node:path";
4
- import { isRecord } from "./actions.js";
5
- import { cloudOrigin } from "./cloud.js";
6
- import { readPrivateJson } from "./storage.js";
7
- export function configRoot() {
8
- return resolve(process.env.ROME_NODE_CONFIG_DIR ||
9
- (process.platform === "win32"
10
- ? join(process.env.LOCALAPPDATA || homedir(), "RomeNode")
11
- : join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "rome-node")));
12
- }
13
- export const callerCredentialPath = () => join(configRoot(), "caller.json");
14
- export const daemonStatePath = () => join(configRoot(), "daemon.json");
15
- export function daemonPort() {
16
- const override = process.env.ROME_NODE_DAEMON_PORT;
17
- if (override !== undefined) {
18
- const port = Number(override);
19
- if (!Number.isInteger(port) || port < 1024 || port > 65535)
20
- throw new Error("ROME_NODE_DAEMON_PORT must be a port between 1024 and 65535.");
21
- return port;
22
- }
23
- return 20000 + (createHash("sha256").update(configRoot()).digest().readUInt32BE(0) % 40000);
24
- }
25
- export async function readCallerCredential() {
26
- const value = await readPrivateJson(callerCredentialPath());
27
- if (!isRecord(value) ||
28
- typeof value.cloudUrl !== "string" ||
29
- typeof value.token !== "string" ||
30
- !/^romedev_[A-Za-z0-9_-]{43}$/.test(value.token))
31
- throw new Error("Configure a communication token with rome-node auth --cloud <origin> using stdin first.");
32
- return { cloudUrl: cloudOrigin(value.cloudUrl), token: value.token };
33
- }
package/dist/login.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export interface DeviceSession {
2
- cloudUrl: string;
3
- token: string;
4
- deviceId: string;
5
- }
6
- export declare function loginDevice(cloudUrl: string, name: string, signal: AbortSignal): Promise<DeviceSession>;
package/dist/login.js DELETED
@@ -1,100 +0,0 @@
1
- import { createServer } from "node:http";
2
- import { createHash, randomBytes } from "node:crypto";
3
- import { platform } from "node:os";
4
- import { cloudOrigin } from "./cloud.js";
5
- import { validId } from "./protocol.js";
6
- export async function loginDevice(cloudUrl, name, signal) {
7
- const cloud = new URL(cloudOrigin(cloudUrl));
8
- const verifier = randomBytes(32).toString("base64url");
9
- const state = randomBytes(32).toString("base64url");
10
- return new Promise((resolve, reject) => {
11
- if (signal.aborted) {
12
- reject(new Error("Authorization canceled."));
13
- return;
14
- }
15
- let redeeming = false;
16
- const server = createServer(async (request, response) => {
17
- const incoming = new URL(request.url ?? "/", "http://127.0.0.1");
18
- if (request.method !== "GET" ||
19
- incoming.pathname !== "/callback" ||
20
- incoming.searchParams.get("state") !== state ||
21
- incoming.searchParams.get("iss") !== cloud.origin ||
22
- !incoming.searchParams.get("code") ||
23
- redeeming) {
24
- response.writeHead(400).end("Invalid authorization callback");
25
- return;
26
- }
27
- redeeming = true;
28
- try {
29
- const result = await fetch(`${cloud.origin}/oauth2/token`, {
30
- method: "POST",
31
- signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]),
32
- redirect: "error",
33
- body: new URLSearchParams({
34
- grant_type: "authorization_code",
35
- client_id: "rome-computer",
36
- redirect_uri: redirectUri,
37
- code: incoming.searchParams.get("code"),
38
- code_verifier: verifier,
39
- platform: platform() === "darwin" ? "macos" : platform() === "win32" ? "windows" : "linux",
40
- }),
41
- });
42
- if (!result.ok)
43
- throw new Error("Token exchange rejected");
44
- const body = (await result.json());
45
- if (typeof body.access_token !== "string" ||
46
- !/^romedev_[A-Za-z0-9_-]{43}$/.test(body.access_token) ||
47
- !validId(body.device_id))
48
- throw new Error("Invalid token response");
49
- response
50
- .writeHead(200, { "content-type": "text/plain", "cache-control": "no-store" })
51
- .end("Device authorized. Return to Rome Node.");
52
- resolve({ cloudUrl: cloud.origin, token: body.access_token, deviceId: body.device_id });
53
- }
54
- catch {
55
- response.writeHead(400).end("Device authorization failed. Run rome-node connect to retry.");
56
- reject(new Error("Device authorization failed"));
57
- }
58
- finally {
59
- clearTimeout(timer);
60
- signal.removeEventListener("abort", abort);
61
- server.close();
62
- }
63
- });
64
- let redirectUri;
65
- const abort = () => {
66
- clearTimeout(timer);
67
- server.closeAllConnections();
68
- server.close();
69
- reject(new Error("Authorization canceled."));
70
- };
71
- const timer = setTimeout(() => {
72
- server.close();
73
- signal.removeEventListener("abort", abort);
74
- reject(new Error("Login timed out"));
75
- }, 5 * 60_000);
76
- signal.addEventListener("abort", abort, { once: true });
77
- server.on("error", () => {
78
- clearTimeout(timer);
79
- signal.removeEventListener("abort", abort);
80
- reject(new Error("Loopback listener failed"));
81
- });
82
- server.listen(0, "127.0.0.1", () => {
83
- const address = server.address();
84
- if (!address || typeof address === "string")
85
- return;
86
- redirectUri = `http://127.0.0.1:${address.port}/callback`;
87
- const url = new URL("/oauth2/authorize", cloud);
88
- url.search = new URLSearchParams({
89
- response_type: "code",
90
- client_id: "rome-computer",
91
- redirect_uri: redirectUri,
92
- code_challenge: createHash("sha256").update(verifier).digest("base64url"),
93
- code_challenge_method: "S256",
94
- state,
95
- display_name: name,
96
- }).toString();
97
- process.stderr.write(`Open this URL in your browser to authorize ${name}:\n${url}\n`);
98
- });
99
- });
100
- }
@@ -1,30 +0,0 @@
1
- export declare const MAX_MESSAGE_BYTES: number;
2
- export declare const MAX_CLIENT_BUFFER_BYTES: number;
3
- export declare const CLOSE: {
4
- readonly revoked: 4001;
5
- readonly superseded: 4002;
6
- readonly outputLimit: 4003;
7
- readonly invalid: 1008;
8
- readonly tooLarge: 1009;
9
- };
10
- export interface OutboundEnvelope {
11
- id: string;
12
- to: string;
13
- payload: unknown;
14
- }
15
- export interface InboundEnvelope {
16
- id: string;
17
- from: string;
18
- payload: unknown;
19
- }
20
- interface RouteError {
21
- id: string;
22
- type: "error";
23
- code: "target_unavailable" | "invalid_message" | "message_too_large";
24
- }
25
- export type GatewayMessage = InboundEnvelope | RouteError;
26
- export declare function validId(value: unknown): value is string;
27
- export declare function outboundEnvelope(value: unknown): OutboundEnvelope | null;
28
- export declare function gatewayMessage(value: unknown): GatewayMessage | null;
29
- export declare function byteLength(value: string): number;
30
- export {};
package/dist/protocol.js DELETED
@@ -1,40 +0,0 @@
1
- export const MAX_MESSAGE_BYTES = 128 * 1024;
2
- export const MAX_CLIENT_BUFFER_BYTES = 1024 * 1024;
3
- export const CLOSE = {
4
- revoked: 4001,
5
- superseded: 4002,
6
- outputLimit: 4003,
7
- invalid: 1008,
8
- tooLarge: 1009,
9
- };
10
- export function validId(value) {
11
- return (typeof value === "string" &&
12
- value.length > 0 &&
13
- value.length <= 128 &&
14
- !/[\u0000-\u0020\u007f]/.test(value));
15
- }
16
- export function outboundEnvelope(value) {
17
- if (!value || typeof value !== "object" || Array.isArray(value))
18
- return null;
19
- const item = value;
20
- if (!validId(item.id) || !validId(item.to) || !Object.hasOwn(item, "payload"))
21
- return null;
22
- return { id: item.id, to: item.to, payload: item.payload };
23
- }
24
- export function gatewayMessage(value) {
25
- if (!value || typeof value !== "object" || Array.isArray(value))
26
- return null;
27
- const item = value;
28
- if (!validId(item.id))
29
- return null;
30
- if (item.type === "error" &&
31
- ["target_unavailable", "invalid_message", "message_too_large"].includes(String(item.code))) {
32
- return item;
33
- }
34
- if (validId(item.from) && Object.hasOwn(item, "payload"))
35
- return { id: item.id, from: item.from, payload: item.payload };
36
- return null;
37
- }
38
- export function byteLength(value) {
39
- return new TextEncoder().encode(value).byteLength;
40
- }
package/dist/socket.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { ClientSocket } from "./client.js";
2
- export declare function createNodeSocket(url: string, authorization: string): ClientSocket;
package/dist/socket.js DELETED
@@ -1,27 +0,0 @@
1
- import WebSocket from "ws";
2
- import { MAX_MESSAGE_BYTES } from "./protocol.js";
3
- export function createNodeSocket(url, authorization) {
4
- const socket = new WebSocket(url, {
5
- headers: { Authorization: authorization },
6
- handshakeTimeout: 10_000,
7
- maxPayload: MAX_MESSAGE_BYTES,
8
- followRedirects: false,
9
- });
10
- let alive = true;
11
- const heartbeat = setInterval(() => {
12
- if (socket.readyState !== WebSocket.OPEN)
13
- return;
14
- if (!alive) {
15
- socket.terminate();
16
- return;
17
- }
18
- alive = false;
19
- socket.ping();
20
- }, 30_000);
21
- heartbeat.unref();
22
- socket.on("pong", () => {
23
- alive = true;
24
- });
25
- socket.on("close", () => clearInterval(heartbeat));
26
- return socket;
27
- }
package/dist/storage.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export declare function readPrivateJson(path: string): Promise<unknown>;
2
- export declare function writePrivateJson(path: string, value: unknown): Promise<void>;
package/dist/storage.js DELETED
@@ -1,37 +0,0 @@
1
- import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- import { randomUUID } from "node:crypto";
4
- import { execFile } from "node:child_process";
5
- import { promisify } from "node:util";
6
- export async function readPrivateJson(path) {
7
- try {
8
- return JSON.parse(await readFile(path, "utf8"));
9
- }
10
- catch (error) {
11
- if (error instanceof SyntaxError || error.code === "ENOENT")
12
- return null;
13
- throw new Error("Could not read the local credential file.");
14
- }
15
- }
16
- export async function writePrivateJson(path, value) {
17
- const dir = dirname(path);
18
- await mkdir(dir, { recursive: true, mode: 0o700 });
19
- if (process.platform === "win32") {
20
- const run = promisify(execFile);
21
- const { stdout } = await run("whoami.exe", ["/user", "/fo", "csv", "/nh"]);
22
- const sid = stdout.match(/S-1-\d+(?:-\d+)+/)?.[0];
23
- if (!sid)
24
- throw new Error("Could not determine the Windows account identity.");
25
- await run("icacls.exe", [dir, "/inheritance:r", "/grant:r", `*${sid}:(OI)(CI)F`]);
26
- }
27
- else
28
- await chmod(dir, 0o700);
29
- const temporary = `${path}.${randomUUID()}.tmp`;
30
- try {
31
- await writeFile(temporary, JSON.stringify(value), { mode: 0o600, flag: "wx" });
32
- await rename(temporary, path);
33
- }
34
- finally {
35
- await rm(temporary, { force: true });
36
- }
37
- }