@scopebond/agent 0.4.6 → 0.5.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.
Files changed (69) hide show
  1. package/README.md +35 -6
  2. package/dist/agent.d.ts +2 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +4 -4
  5. package/dist/agent.js.map +1 -1
  6. package/dist/allowance-sender.d.ts +8 -0
  7. package/dist/allowance-sender.d.ts.map +1 -0
  8. package/dist/allowance-sender.js +80 -0
  9. package/dist/allowance-sender.js.map +1 -0
  10. package/dist/autostart.d.ts +13 -0
  11. package/dist/autostart.d.ts.map +1 -1
  12. package/dist/autostart.js +60 -10
  13. package/dist/autostart.js.map +1 -1
  14. package/dist/cli-main.d.ts +3 -0
  15. package/dist/cli-main.d.ts.map +1 -0
  16. package/dist/cli-main.js +218 -0
  17. package/dist/cli-main.js.map +1 -0
  18. package/dist/cli.js +4 -185
  19. package/dist/cli.js.map +1 -1
  20. package/dist/index.d.ts +7 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +5 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/ipc.d.ts +7 -1
  25. package/dist/ipc.d.ts.map +1 -1
  26. package/dist/ipc.js +64 -12
  27. package/dist/ipc.js.map +1 -1
  28. package/dist/log-file.d.ts +6 -0
  29. package/dist/log-file.d.ts.map +1 -0
  30. package/dist/log-file.js +40 -0
  31. package/dist/log-file.js.map +1 -0
  32. package/dist/native-update.d.ts +51 -0
  33. package/dist/native-update.d.ts.map +1 -0
  34. package/dist/native-update.js +120 -0
  35. package/dist/native-update.js.map +1 -0
  36. package/dist/prompt.d.ts +11 -1
  37. package/dist/prompt.d.ts.map +1 -1
  38. package/dist/prompt.js +57 -23
  39. package/dist/prompt.js.map +1 -1
  40. package/dist/reconnect.d.ts +19 -0
  41. package/dist/reconnect.d.ts.map +1 -0
  42. package/dist/reconnect.js +60 -0
  43. package/dist/reconnect.js.map +1 -0
  44. package/dist/self.d.ts +5 -0
  45. package/dist/self.d.ts.map +1 -0
  46. package/dist/self.js +15 -0
  47. package/dist/self.js.map +1 -0
  48. package/dist/service.d.ts +50 -4
  49. package/dist/service.d.ts.map +1 -1
  50. package/dist/service.js +292 -18
  51. package/dist/service.js.map +1 -1
  52. package/dist/setup.d.ts.map +1 -1
  53. package/dist/setup.js +4 -9
  54. package/dist/setup.js.map +1 -1
  55. package/dist/summary.d.ts +16 -0
  56. package/dist/summary.d.ts.map +1 -0
  57. package/dist/summary.js +47 -0
  58. package/dist/summary.js.map +1 -0
  59. package/dist/tray-model.d.ts +91 -0
  60. package/dist/tray-model.d.ts.map +1 -0
  61. package/dist/tray-model.js +159 -0
  62. package/dist/tray-model.js.map +1 -0
  63. package/dist/tray.d.ts.map +1 -1
  64. package/dist/tray.js +103 -29
  65. package/dist/tray.js.map +1 -1
  66. package/dist/update.d.ts.map +1 -1
  67. package/dist/update.js +7 -4
  68. package/dist/update.js.map +1 -1
  69. package/package.json +4 -4
package/dist/ipc.js CHANGED
@@ -1,13 +1,28 @@
1
- // The agent's local control channel: an HTTP server bound to 127.0.0.1 on a random port, with a
2
- // random token. Port, token and process id are written to agent.json in the Scopebond home (owner
3
- // read/write only), which is how the CLI and, later, the tray find it. Nothing listens beyond this
4
- // computer, and every request must carry the token.
1
+ // The agent's local control channel: HTTP with a random token, over a named pipe on Windows (a random name) or a Unix
2
+ // socket in a folder only this user can open, and for one more release also on 127.0.0.1 at a random port (the PowerShell
3
+ // tray uses it; SCOPEBOND_AGENT_LOOPBACK=0 turns it off). The pipe or socket, the port, the token and the process id are
4
+ // written to agent.json in the Scopebond home, which is how the CLI, the hook's override window and the tray find it.
5
+ // A loopback port can be reached by any program on the computer; the pipe's name and the socket's folder cannot be found
6
+ // or opened by another user. Every request must carry the token either way.
5
7
  import { createServer } from "node:http";
6
8
  import { randomBytes, timingSafeEqual } from "node:crypto";
7
- import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
9
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
8
11
  import { join } from "node:path";
12
+ import { requestOverSocket } from "@scopebond/hook";
9
13
  export const AGENT_FILE = "agent.json";
10
14
  export const TOKEN_HEADER = "x-scopebond-agent-token";
15
+ /** Where this start's pipe or socket goes. Windows: a pipe whose name is random, so no other user can guess it or claim it
16
+ * first. Elsewhere: `run/agent.sock` in the Scopebond home (folder 0700), or a private folder under the temporary folder
17
+ * when that path is longer than a Unix socket path may be. */
18
+ export function localSocketPath(dir, platform = process.platform, random = randomBytes(16).toString("hex")) {
19
+ if (platform === "win32")
20
+ return `\\\\.\\pipe\\scopebond-agent-${random}`;
21
+ const inHome = join(dir, "run", "agent.sock");
22
+ if (Buffer.byteLength(inHome) <= 100)
23
+ return inHome;
24
+ return join(mkdtempSync(join(tmpdir(), "scopebond-")), "agent.sock");
25
+ }
11
26
  export function readEndpoint(dir) {
12
27
  const file = join(dir, AGENT_FILE);
13
28
  if (!existsSync(file))
@@ -23,7 +38,7 @@ export function readEndpoint(dir) {
23
38
  export async function startControl(dir, version, routes) {
24
39
  const token = randomBytes(24).toString("base64url");
25
40
  const expected = Buffer.from(token);
26
- const server = createServer(async (req, res) => {
41
+ const handle = async (req, res) => {
27
42
  const send = (status, body) => { res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" }); res.end(JSON.stringify(body)); };
28
43
  const offered = Buffer.from(String(req.headers[TOKEN_HEADER] ?? ""));
29
44
  if (offered.length !== expected.length || !timingSafeEqual(offered, expected))
@@ -43,11 +58,32 @@ export async function startControl(dir, version, routes) {
43
58
  catch (error) {
44
59
  send(500, { error: error.message.slice(0, 200) });
45
60
  }
46
- });
47
- await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => resolve()); });
48
- const address = server.address();
61
+ };
62
+ // The pipe or socket first. A Unix socket's folder is created for this user only, and the socket itself is too.
63
+ const socketPath = localSocketPath(dir);
64
+ const local = createServer(handle);
65
+ let socket = socketPath;
66
+ try {
67
+ if (process.platform !== "win32") {
68
+ mkdirSync(join(socketPath, ".."), { recursive: true, mode: 0o700 });
69
+ chmodSync(join(socketPath, ".."), 0o700);
70
+ rmSync(socketPath, { force: true });
71
+ }
72
+ await new Promise((resolve, reject) => { local.once("error", reject); local.listen(socketPath, () => resolve()); });
73
+ if (process.platform !== "win32")
74
+ chmodSync(socketPath, 0o600);
75
+ }
76
+ catch {
77
+ socket = null;
78
+ }
79
+ // Loopback for one more release (the PowerShell tray), unless turned off; always when the pipe could not be made.
80
+ const loopback = process.env.SCOPEBOND_AGENT_LOOPBACK !== "0" || socket === null;
81
+ const server = createServer(handle);
82
+ if (loopback)
83
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => resolve()); });
84
+ const address = loopback ? server.address() : null;
49
85
  const port = typeof address === "object" && address ? address.port : 0;
50
- const endpoint = { port, token, pid: process.pid, started_at: Date.now(), version };
86
+ const endpoint = { port, socket, token, pid: process.pid, started_at: Date.now(), version };
51
87
  const file = join(dir, AGENT_FILE);
52
88
  const temp = `${file}.${process.pid}.tmp`;
53
89
  writeFileSync(temp, JSON.stringify(endpoint, null, 2) + "\n", { mode: 0o600 });
@@ -55,7 +91,16 @@ export async function startControl(dir, version, routes) {
55
91
  return {
56
92
  server, endpoint,
57
93
  close: () => new Promise((resolve) => {
58
- server.close(() => resolve());
94
+ let open = (loopback ? 1 : 0) + (socket ? 1 : 0);
95
+ const done = () => { if (--open <= 0)
96
+ resolve(); };
97
+ if (open === 0)
98
+ resolve();
99
+ if (loopback)
100
+ server.close(done);
101
+ if (socket)
102
+ local.close(() => { if (process.platform !== "win32")
103
+ rmSync(socketPath, { force: true }); done(); });
59
104
  // Only remove the file if it is still ours (a newer agent may have replaced it).
60
105
  try {
61
106
  if (readEndpoint(dir)?.pid === process.pid)
@@ -65,11 +110,18 @@ export async function startControl(dir, version, routes) {
65
110
  }),
66
111
  };
67
112
  }
68
- /** Call a running agent. Returns null when no agent answers. */
113
+ /** Call a running agent, over its pipe or socket when it has one. Returns null when no agent answers. */
69
114
  export async function callAgent(dir, method, path, body, timeoutMs = 5_000) {
70
115
  const endpoint = readEndpoint(dir);
71
116
  if (!endpoint)
72
117
  return null;
118
+ if (typeof endpoint.socket === "string" && endpoint.socket) {
119
+ const answer = await requestOverSocket(endpoint.socket, method, path, { [TOKEN_HEADER]: endpoint.token }, body, timeoutMs);
120
+ if (answer)
121
+ return answer.status >= 200 && answer.status < 300 ? answer.body : null;
122
+ if (!endpoint.port)
123
+ return null;
124
+ }
73
125
  try {
74
126
  const res = await fetch(`http://127.0.0.1:${endpoint.port}${path}`, {
75
127
  method, headers: { [TOKEN_HEADER]: endpoint.token, ...(body !== undefined ? { "content-type": "application/json" } : {}) },
package/dist/ipc.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"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","sourcesContent":["// The agent's local control channel: an HTTP server bound to 127.0.0.1 on a random port, with a\n// random token. Port, token and process id are written to agent.json in the Scopebond home (owner\n// read/write only), which is how the CLI and, later, the tray find it. Nothing listens beyond this\n// computer, and every request must carry the token.\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport const AGENT_FILE = \"agent.json\";\nexport const TOKEN_HEADER = \"x-scopebond-agent-token\";\n\nexport interface AgentEndpoint { port: number; token: string; pid: number; started_at: number; version: string }\n\nexport type Handler = (body: unknown) => Promise<unknown> | unknown;\n\nexport function readEndpoint(dir: string): AgentEndpoint | null {\n const file = join(dir, AGENT_FILE);\n if (!existsSync(file)) return null;\n try { return JSON.parse(readFileSync(file, \"utf8\")) as AgentEndpoint; } catch { return null; }\n}\n\n/** Start the control server. `routes` maps \"METHOD /path\" to a handler. */\nexport async function startControl(dir: string, version: string, routes: Record<string, Handler>): Promise<{ server: Server; endpoint: AgentEndpoint; close(): Promise<void> }> {\n const token = randomBytes(24).toString(\"base64url\");\n const expected = Buffer.from(token);\n const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const send = (status: number, body: unknown) => { res.writeHead(status, { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" }); res.end(JSON.stringify(body)); };\n const offered = Buffer.from(String(req.headers[TOKEN_HEADER] ?? \"\"));\n if (offered.length !== expected.length || !timingSafeEqual(offered, expected)) return send(401, { error: \"unauthorized\" });\n const handler = routes[`${req.method} ${(req.url ?? \"/\").split(\"?\")[0]}`];\n if (!handler) return send(404, { error: \"not found\" });\n let raw = \"\";\n for await (const chunk of req) { raw += chunk; if (raw.length > 64 * 1024) return send(413, { error: \"too large\" }); }\n try { send(200, await handler(raw ? JSON.parse(raw) : null)); }\n catch (error) { send(500, { error: (error as Error).message.slice(0, 200) }); }\n });\n await new Promise<void>((resolve, reject) => { server.once(\"error\", reject); server.listen(0, \"127.0.0.1\", () => resolve()); });\n const address = server.address();\n const port = typeof address === \"object\" && address ? address.port : 0;\n const endpoint: AgentEndpoint = { port, token, pid: process.pid, started_at: Date.now(), version };\n const file = join(dir, AGENT_FILE);\n const temp = `${file}.${process.pid}.tmp`;\n writeFileSync(temp, JSON.stringify(endpoint, null, 2) + \"\\n\", { mode: 0o600 });\n renameSync(temp, file);\n return {\n server, endpoint,\n close: () => new Promise<void>((resolve) => {\n server.close(() => resolve());\n // Only remove the file if it is still ours (a newer agent may have replaced it).\n try { if (readEndpoint(dir)?.pid === process.pid) rmSync(file, { force: true }); } catch { /* best effort */ }\n }),\n };\n}\n\n/** Call a running agent. Returns null when no agent answers. */\nexport async function callAgent(dir: string, method: \"GET\" | \"POST\", path: string, body?: unknown, timeoutMs = 5_000): Promise<unknown | null> {\n const endpoint = readEndpoint(dir);\n if (!endpoint) return null;\n try {\n const res = await fetch(`http://127.0.0.1:${endpoint.port}${path}`, {\n method, headers: { [TOKEN_HEADER]: endpoint.token, ...(body !== undefined ? { \"content-type\": \"application/json\" } : {}) },\n body: body !== undefined ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(timeoutMs),\n });\n return res.ok ? await res.json() : null;\n } catch { return null; }\n}\n"]}
1
+ {"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"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","sourcesContent":["// The agent's local control channel: HTTP with a random token, over a named pipe on Windows (a random name) or a Unix\n// socket in a folder only this user can open, and for one more release also on 127.0.0.1 at a random port (the PowerShell\n// tray uses it; SCOPEBOND_AGENT_LOOPBACK=0 turns it off). The pipe or socket, the port, the token and the process id are\n// written to agent.json in the Scopebond home, which is how the CLI, the hook's override window and the tray find it.\n// A loopback port can be reached by any program on the computer; the pipe's name and the socket's folder cannot be found\n// or opened by another user. Every request must carry the token either way.\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { requestOverSocket } from \"@scopebond/hook\";\n\nexport const AGENT_FILE = \"agent.json\";\nexport const TOKEN_HEADER = \"x-scopebond-agent-token\";\n\n/** `socket`: the named pipe or Unix socket (absent from agents before it); `port`: 0 when loopback is off. */\nexport interface AgentEndpoint { port: number; socket?: string | null; token: string; pid: number; started_at: number; version: string }\n\n/** Where this start's pipe or socket goes. Windows: a pipe whose name is random, so no other user can guess it or claim it\n * first. Elsewhere: `run/agent.sock` in the Scopebond home (folder 0700), or a private folder under the temporary folder\n * when that path is longer than a Unix socket path may be. */\nexport function localSocketPath(dir: string, platform: NodeJS.Platform = process.platform, random = randomBytes(16).toString(\"hex\")): string {\n if (platform === \"win32\") return `\\\\\\\\.\\\\pipe\\\\scopebond-agent-${random}`;\n const inHome = join(dir, \"run\", \"agent.sock\");\n if (Buffer.byteLength(inHome) <= 100) return inHome;\n return join(mkdtempSync(join(tmpdir(), \"scopebond-\")), \"agent.sock\");\n}\n\nexport type Handler = (body: unknown) => Promise<unknown> | unknown;\n\nexport function readEndpoint(dir: string): AgentEndpoint | null {\n const file = join(dir, AGENT_FILE);\n if (!existsSync(file)) return null;\n try { return JSON.parse(readFileSync(file, \"utf8\")) as AgentEndpoint; } catch { return null; }\n}\n\n/** Start the control server. `routes` maps \"METHOD /path\" to a handler. */\nexport async function startControl(dir: string, version: string, routes: Record<string, Handler>): Promise<{ server: Server; endpoint: AgentEndpoint; close(): Promise<void> }> {\n const token = randomBytes(24).toString(\"base64url\");\n const expected = Buffer.from(token);\n const handle = async (req: IncomingMessage, res: ServerResponse) => {\n const send = (status: number, body: unknown) => { res.writeHead(status, { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" }); res.end(JSON.stringify(body)); };\n const offered = Buffer.from(String(req.headers[TOKEN_HEADER] ?? \"\"));\n if (offered.length !== expected.length || !timingSafeEqual(offered, expected)) return send(401, { error: \"unauthorized\" });\n const handler = routes[`${req.method} ${(req.url ?? \"/\").split(\"?\")[0]}`];\n if (!handler) return send(404, { error: \"not found\" });\n let raw = \"\";\n for await (const chunk of req) { raw += chunk; if (raw.length > 64 * 1024) return send(413, { error: \"too large\" }); }\n try { send(200, await handler(raw ? JSON.parse(raw) : null)); }\n catch (error) { send(500, { error: (error as Error).message.slice(0, 200) }); }\n };\n // The pipe or socket first. A Unix socket's folder is created for this user only, and the socket itself is too.\n const socketPath = localSocketPath(dir);\n const local = createServer(handle);\n let socket: string | null = socketPath;\n try {\n if (process.platform !== \"win32\") {\n mkdirSync(join(socketPath, \"..\"), { recursive: true, mode: 0o700 });\n chmodSync(join(socketPath, \"..\"), 0o700);\n rmSync(socketPath, { force: true });\n }\n await new Promise<void>((resolve, reject) => { local.once(\"error\", reject); local.listen(socketPath, () => resolve()); });\n if (process.platform !== \"win32\") chmodSync(socketPath, 0o600);\n } catch { socket = null; }\n // Loopback for one more release (the PowerShell tray), unless turned off; always when the pipe could not be made.\n const loopback = process.env.SCOPEBOND_AGENT_LOOPBACK !== \"0\" || socket === null;\n const server = createServer(handle);\n if (loopback) await new Promise<void>((resolve, reject) => { server.once(\"error\", reject); server.listen(0, \"127.0.0.1\", () => resolve()); });\n const address = loopback ? server.address() : null;\n const port = typeof address === \"object\" && address ? address.port : 0;\n const endpoint: AgentEndpoint = { port, socket, token, pid: process.pid, started_at: Date.now(), version };\n const file = join(dir, AGENT_FILE);\n const temp = `${file}.${process.pid}.tmp`;\n writeFileSync(temp, JSON.stringify(endpoint, null, 2) + \"\\n\", { mode: 0o600 });\n renameSync(temp, file);\n return {\n server, endpoint,\n close: () => new Promise<void>((resolve) => {\n let open = (loopback ? 1 : 0) + (socket ? 1 : 0);\n const done = () => { if (--open <= 0) resolve(); };\n if (open === 0) resolve();\n if (loopback) server.close(done);\n if (socket) local.close(() => { if (process.platform !== \"win32\") rmSync(socketPath, { force: true }); done(); });\n // Only remove the file if it is still ours (a newer agent may have replaced it).\n try { if (readEndpoint(dir)?.pid === process.pid) rmSync(file, { force: true }); } catch { /* best effort */ }\n }),\n };\n}\n\n/** Call a running agent, over its pipe or socket when it has one. Returns null when no agent answers. */\nexport async function callAgent(dir: string, method: \"GET\" | \"POST\", path: string, body?: unknown, timeoutMs = 5_000): Promise<unknown | null> {\n const endpoint = readEndpoint(dir);\n if (!endpoint) return null;\n if (typeof endpoint.socket === \"string\" && endpoint.socket) {\n const answer = await requestOverSocket(endpoint.socket, method, path, { [TOKEN_HEADER]: endpoint.token }, body, timeoutMs);\n if (answer) return answer.status >= 200 && answer.status < 300 ? answer.body : null;\n if (!endpoint.port) return null;\n }\n try {\n const res = await fetch(`http://127.0.0.1:${endpoint.port}${path}`, {\n method, headers: { [TOKEN_HEADER]: endpoint.token, ...(body !== undefined ? { \"content-type\": \"application/json\" } : {}) },\n body: body !== undefined ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(timeoutMs),\n });\n return res.ok ? await res.json() : null;\n } catch { return null; }\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /** Where lines go while the log itself cannot be opened: an agent started by an older launcher's agent inherits that
2
+ * launcher's handle on agent.log, which keeps everyone else from writing it until the computer signs in again. */
3
+ export declare const FALLBACK_LOG = "agent-handover.log";
4
+ /** Send everything this process writes to stdout and stderr, and an error that would end it, to `file`. */
5
+ export declare function writeOutputTo(file: string): void;
6
+ //# sourceMappingURL=log-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-file.d.ts","sourceRoot":"","sources":["../src/log-file.ts"],"names":[],"mappings":"AAQA;mHACmH;AACnH,eAAO,MAAM,YAAY,uBAAuB,CAAC;AAEjD,2GAA2G;AAC3G,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAoBhD"}
@@ -0,0 +1,40 @@
1
+ // The agent's own log. A launcher redirecting the agent's output into the log would hold the file open for as long as
2
+ // the agent runs, shared for reading only on Windows, and every child of the agent would inherit that handle. Writing it
3
+ // here instead opens the file for each line, shared for reading and writing, so two agents (an old one handing over to
4
+ // its update) can both write to it and no handle outlives a line.
5
+ import { appendFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ /** Where lines go while the log itself cannot be opened: an agent started by an older launcher's agent inherits that
8
+ * launcher's handle on agent.log, which keeps everyone else from writing it until the computer signs in again. */
9
+ export const FALLBACK_LOG = "agent-handover.log";
10
+ /** Send everything this process writes to stdout and stderr, and an error that would end it, to `file`. */
11
+ export function writeOutputTo(file) {
12
+ const fallback = join(dirname(file), FALLBACK_LOG);
13
+ const append = (data) => {
14
+ try {
15
+ appendFileSync(file, data);
16
+ }
17
+ catch {
18
+ try {
19
+ appendFileSync(fallback, data);
20
+ }
21
+ catch { /* nowhere to write: the line is lost, the agent runs on */ }
22
+ }
23
+ };
24
+ for (const stream of [process.stdout, process.stderr]) {
25
+ stream.write = ((data, encoding, callback) => {
26
+ append(data);
27
+ const done = typeof encoding === "function" ? encoding : callback;
28
+ if (typeof done === "function")
29
+ queueMicrotask(() => done());
30
+ return true;
31
+ });
32
+ }
33
+ // Node prints a fatal error straight to the console, which is nul here: note it in the log, then exit as Node would,
34
+ // so the launcher restarts the agent.
35
+ process.on("uncaughtException", (error) => {
36
+ append(`${new Date().toISOString()} the agent stopped on an error: ${error?.stack ?? String(error)}\n`);
37
+ process.exit(1);
38
+ });
39
+ }
40
+ //# sourceMappingURL=log-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-file.js","sourceRoot":"","sources":["../src/log-file.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,yHAAyH;AACzH,uHAAuH;AACvH,kEAAkE;AAElE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C;mHACmH;AACnH,MAAM,CAAC,MAAM,YAAY,GAAG,oBAAoB,CAAC;AAEjD,2GAA2G;AAC3G,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,CAAC,IAAyB,EAAE,EAAE;QAC3C,IAAI,CAAC;YAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;QACnC,MAAM,CAAC;YAAC,IAAI,CAAC;gBAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,2DAA2D,CAAC,CAAC;QAAC,CAAC;IACzH,CAAC,CAAC;IACF,KAAK,MAAM,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACtD,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,IAAyB,EAAE,QAAkB,EAAE,QAAkB,EAAE,EAAE;YACpF,MAAM,CAAC,IAAI,CAAC,CAAC;YACb,MAAM,IAAI,GAAG,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;YAClE,IAAI,OAAO,IAAI,KAAK,UAAU;gBAAE,cAAc,CAAC,GAAG,EAAE,CAAE,IAAmB,EAAE,CAAC,CAAC;YAC7E,OAAO,IAAI,CAAC;QACd,CAAC,CAAwB,CAAC;IAC5B,CAAC;IACD,qHAAqH;IACrH,sCAAsC;IACtC,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE;QACxC,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,mCAAmC,KAAK,EAAE,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACxG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// The agent's own log. A launcher redirecting the agent's output into the log would hold the file open for as long as\n// the agent runs, shared for reading only on Windows, and every child of the agent would inherit that handle. Writing it\n// here instead opens the file for each line, shared for reading and writing, so two agents (an old one handing over to\n// its update) can both write to it and no handle outlives a line.\n\nimport { appendFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Where lines go while the log itself cannot be opened: an agent started by an older launcher's agent inherits that\n * launcher's handle on agent.log, which keeps everyone else from writing it until the computer signs in again. */\nexport const FALLBACK_LOG = \"agent-handover.log\";\n\n/** Send everything this process writes to stdout and stderr, and an error that would end it, to `file`. */\nexport function writeOutputTo(file: string): void {\n const fallback = join(dirname(file), FALLBACK_LOG);\n const append = (data: string | Uint8Array) => {\n try { appendFileSync(file, data); }\n catch { try { appendFileSync(fallback, data); } catch { /* nowhere to write: the line is lost, the agent runs on */ } }\n };\n for (const stream of [process.stdout, process.stderr]) {\n stream.write = ((data: string | Uint8Array, encoding?: unknown, callback?: unknown) => {\n append(data);\n const done = typeof encoding === \"function\" ? encoding : callback;\n if (typeof done === \"function\") queueMicrotask(() => (done as () => void)());\n return true;\n }) as typeof stream.write;\n }\n // Node prints a fatal error straight to the console, which is nul here: note it in the log, then exit as Node would,\n // so the launcher restarts the agent.\n process.on(\"uncaughtException\", (error) => {\n append(`${new Date().toISOString()} the agent stopped on an error: ${error?.stack ?? String(error)}\\n`);\n process.exit(1);\n });\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /** Where the signed releases are published. */
2
+ export declare const RELEASE_BASE = "https://github.com/avouro-com/scopebond/releases/download";
3
+ /** The domain the updater key signs in: a manifest, never anything else. */
4
+ export declare const MANIFEST_DOMAIN = "scopebond:agent-release/v1\n";
5
+ /** The publisher an installer must be signed by. */
6
+ export declare const PUBLISHER: RegExp;
7
+ export declare function updaterPublicKey(): string | null;
8
+ export type InstallKind = "npm" | "per-user" | "per-machine";
9
+ /** How this agent was installed: from npm, by the installer for this user, or for every user (Program Files). */
10
+ export declare function installKind(execPath?: string, single?: boolean, programFiles?: string): InstallKind;
11
+ export interface ReleaseManifest {
12
+ type: "scopebond:agent-release";
13
+ version: string;
14
+ files: Array<{
15
+ name: string;
16
+ sha256: string;
17
+ size: number;
18
+ }>;
19
+ }
20
+ /** The manifest when its signature is the updater key's over exactly these bytes, else null. */
21
+ export declare function verifyManifest(text: string, signatureB64: string, publicKeyB64: string): ReleaseManifest | null;
22
+ export declare const installerName: (version: string) => string;
23
+ /** The Authenticode check: Windows' own verdict on the file, and the signer's subject. */
24
+ export declare function authenticode(file: string): {
25
+ valid: boolean;
26
+ subject: string;
27
+ };
28
+ export interface VerifiedInstaller {
29
+ ok: true;
30
+ path: string;
31
+ version: string;
32
+ }
33
+ export interface Refused {
34
+ ok: false;
35
+ reason: string;
36
+ }
37
+ /** Download the installer for `version` and check it three ways. Nothing is kept unless every check passes. */
38
+ export declare function fetchVerifiedInstaller(version: string, o: {
39
+ dir: string;
40
+ fetchImpl?: typeof fetch;
41
+ publicKey?: string | null;
42
+ check?: (file: string) => {
43
+ valid: boolean;
44
+ subject: string;
45
+ };
46
+ base?: string;
47
+ }): Promise<VerifiedInstaller | Refused>;
48
+ /** Install it once this agent has exited, then start the agent again through autostart's launcher. A detached helper does it,
49
+ * because the installer replaces this very file. */
50
+ export declare function installAfterExit(installer: string, pid: number, launcher: string | null): void;
51
+ //# sourceMappingURL=native-update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-update.d.ts","sourceRoot":"","sources":["../src/native-update.ts"],"names":[],"mappings":"AAcA,+CAA+C;AAC/C,eAAO,MAAM,YAAY,8DAA8D,CAAC;AACxF,4EAA4E;AAC5E,eAAO,MAAM,eAAe,iCAAiC,CAAC;AAC9D,oDAAoD;AACpD,eAAO,MAAM,SAAS,QAA8B,CAAC;AAKrD,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,UAAU,GAAG,aAAa,CAAC;AAE7D,iHAAiH;AACjH,wBAAgB,WAAW,CAAC,QAAQ,SAAmB,EAAE,MAAM,UAAuB,EAAE,YAAY,SAAkD,GAAG,WAAW,CAMnK;AAED,MAAM,WAAW,eAAe;IAAG,IAAI,EAAE,yBAAyB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE;AAEnJ,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAQ/G;AAED,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,WAAyC,CAAC;AAEvF,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAK9E;AAED,MAAM,WAAW,iBAAiB;IAAG,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC9E,MAAM,WAAW,OAAO;IAAG,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAEtD,+GAA+G;AAC/G,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE;IAC/D,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CAChJ,GAAG,OAAO,CAAC,iBAAiB,GAAG,OAAO,CAAC,CA8BvC;AAED;qDACqD;AACrD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAY9F"}
@@ -0,0 +1,120 @@
1
+ // Updates for the signed Windows install (the single executable from the installer). npm installs keep updating with npm.
2
+ //
3
+ // A per-user install updates itself, but only with an installer it has checked three ways: the release manifest is signed
4
+ // with the updater key (a separate Ed25519 key; its public half is built into this program), the installer's SHA-256 and
5
+ // size are the manifest's, and the installer's Authenticode signature is valid and names Avouro LLC. Anything else and
6
+ // nothing is installed: the agent says the update could not be verified. An install for every user (Program Files) never
7
+ // updates itself; the organisation that deployed it updates it.
8
+ import { createHash, createPublicKey, verify } from "node:crypto";
9
+ import { spawn, spawnSync } from "node:child_process";
10
+ import { mkdirSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { isSingleExecutable } from "@scopebond/hook";
13
+ /** Where the signed releases are published. */
14
+ export const RELEASE_BASE = "https://github.com/avouro-com/scopebond/releases/download";
15
+ /** The domain the updater key signs in: a manifest, never anything else. */
16
+ export const MANIFEST_DOMAIN = "scopebond:agent-release/v1\n";
17
+ /** The publisher an installer must be signed by. */
18
+ export const PUBLISHER = /(^|,\s*)O=Avouro LLC(,|$)/;
19
+ export function updaterPublicKey() {
20
+ return typeof __SCOPEBOND_UPDATER_KEY__ === "string" && __SCOPEBOND_UPDATER_KEY__ ? __SCOPEBOND_UPDATER_KEY__ : null;
21
+ }
22
+ /** How this agent was installed: from npm, by the installer for this user, or for every user (Program Files). */
23
+ export function installKind(execPath = process.execPath, single = isSingleExecutable(), programFiles = process.env.ProgramFiles ?? "C:\\Program Files") {
24
+ if (!single)
25
+ return "npm";
26
+ // The folder without trailing separators (a loop, not a regular expression, on a value from the environment).
27
+ let folder = programFiles.toLowerCase();
28
+ while (folder.endsWith("\\") || folder.endsWith("/"))
29
+ folder = folder.slice(0, -1);
30
+ return execPath.toLowerCase().startsWith(`${folder}\\`) ? "per-machine" : "per-user";
31
+ }
32
+ /** The manifest when its signature is the updater key's over exactly these bytes, else null. */
33
+ export function verifyManifest(text, signatureB64, publicKeyB64) {
34
+ try {
35
+ const key = createPublicKey({ key: Buffer.from(publicKeyB64, "base64"), format: "der", type: "spki" });
36
+ if (!verify(null, Buffer.from(MANIFEST_DOMAIN + text, "utf8"), key, Buffer.from(signatureB64.trim(), "base64")))
37
+ return null;
38
+ const manifest = JSON.parse(text);
39
+ if (manifest.type !== "scopebond:agent-release" || typeof manifest.version !== "string" || !Array.isArray(manifest.files))
40
+ return null;
41
+ return manifest;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ export const installerName = (version) => `scopebond-agent-${version}-x64.msi`;
48
+ /** The Authenticode check: Windows' own verdict on the file, and the signer's subject. */
49
+ export function authenticode(file) {
50
+ const script = `$s = Get-AuthenticodeSignature -LiteralPath $env:SB_FILE; [Console]::Out.Write((@{ status = "$($s.Status)"; subject = "$($s.SignerCertificate.Subject)" } | ConvertTo-Json -Compress))`;
51
+ const run = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8", env: { ...process.env, SB_FILE: file }, windowsHide: true, timeout: 60_000 });
52
+ try {
53
+ const r = JSON.parse(run.stdout);
54
+ return { valid: r.status === "Valid", subject: r.subject };
55
+ }
56
+ catch {
57
+ return { valid: false, subject: "" };
58
+ }
59
+ }
60
+ /** Download the installer for `version` and check it three ways. Nothing is kept unless every check passes. */
61
+ export async function fetchVerifiedInstaller(version, o) {
62
+ const publicKey = o.publicKey === undefined ? updaterPublicKey() : o.publicKey;
63
+ if (!publicKey)
64
+ return { ok: false, reason: "this build has no updater key" };
65
+ if (!/^\d+\.\d+\.\d+$/.test(version))
66
+ return { ok: false, reason: "not a version" };
67
+ const fetchImpl = o.fetchImpl ?? fetch;
68
+ const base = `${o.base ?? RELEASE_BASE}/agent-native-v${version}`;
69
+ const get = async (name) => {
70
+ try {
71
+ const res = await fetchImpl(`${base}/${name}`, { redirect: "follow", signal: AbortSignal.timeout(5 * 60_000) });
72
+ return res.ok ? Buffer.from(await res.arrayBuffer()) : null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ };
78
+ const [text, signature] = await Promise.all([get(`scopebond-agent-${version}.manifest.json`), get(`scopebond-agent-${version}.manifest.json.sig`)]);
79
+ if (!text || !signature)
80
+ return { ok: false, reason: "the release has no signed manifest" };
81
+ const manifest = verifyManifest(text.toString("utf8"), signature.toString("utf8"), publicKey);
82
+ if (!manifest)
83
+ return { ok: false, reason: "the release manifest is not signed by the updater key" };
84
+ if (manifest.version !== version)
85
+ return { ok: false, reason: `the manifest is for ${manifest.version}, not ${version}` };
86
+ const entry = manifest.files.find((f) => f.name === installerName(version));
87
+ if (!entry)
88
+ return { ok: false, reason: "the manifest names no installer" };
89
+ const bytes = await get(entry.name);
90
+ if (!bytes)
91
+ return { ok: false, reason: "the installer could not be downloaded" };
92
+ if (bytes.length !== entry.size || createHash("sha256").update(bytes).digest("hex") !== entry.sha256)
93
+ return { ok: false, reason: "the installer is not the one the manifest names" };
94
+ const folder = join(o.dir, "updates");
95
+ mkdirSync(folder, { recursive: true });
96
+ const path = join(folder, entry.name);
97
+ writeFileSync(path, bytes);
98
+ const signed = (o.check ?? authenticode)(path);
99
+ if (!signed.valid)
100
+ return { ok: false, reason: "the installer's signature is not valid" };
101
+ if (!PUBLISHER.test(signed.subject))
102
+ return { ok: false, reason: `the installer is signed by ${signed.subject || "someone else"}, not Avouro LLC` };
103
+ return { ok: true, path, version };
104
+ }
105
+ /** Install it once this agent has exited, then start the agent again through autostart's launcher. A detached helper does it,
106
+ * because the installer replaces this very file. */
107
+ export function installAfterExit(installer, pid, launcher) {
108
+ const script = [
109
+ "$ErrorActionPreference = 'SilentlyContinue'",
110
+ "Wait-Process -Id ([int]$env:SB_PID) -Timeout 60",
111
+ "Start-Process -FilePath msiexec.exe -ArgumentList @('/i', ('\"' + $env:SB_MSI + '\"'), '/qn', '/norestart') -Wait",
112
+ "if ($env:SB_LAUNCHER) { Start-Process -FilePath cmd.exe -ArgumentList @('/d', '/s', '/c', ('\"\"' + $env:SB_LAUNCHER + '\"\"')) -WindowStyle Hidden }",
113
+ ].join("; ");
114
+ const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script], {
115
+ detached: true, stdio: "ignore", windowsHide: true, env: { ...process.env, SB_PID: String(pid), SB_MSI: installer, SB_LAUNCHER: launcher ?? "" },
116
+ });
117
+ child.on("error", () => { });
118
+ child.unref();
119
+ }
120
+ //# sourceMappingURL=native-update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-update.js","sourceRoot":"","sources":["../src/native-update.ts"],"names":[],"mappings":"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","sourcesContent":["// Updates for the signed Windows install (the single executable from the installer). npm installs keep updating with npm.\n//\n// A per-user install updates itself, but only with an installer it has checked three ways: the release manifest is signed\n// with the updater key (a separate Ed25519 key; its public half is built into this program), the installer's SHA-256 and\n// size are the manifest's, and the installer's Authenticode signature is valid and names Avouro LLC. Anything else and\n// nothing is installed: the agent says the update could not be verified. An install for every user (Program Files) never\n// updates itself; the organisation that deployed it updates it.\n\nimport { createHash, createPublicKey, verify } from \"node:crypto\";\nimport { spawn, spawnSync } from \"node:child_process\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { isSingleExecutable } from \"@scopebond/hook\";\n\n/** Where the signed releases are published. */\nexport const RELEASE_BASE = \"https://github.com/avouro-com/scopebond/releases/download\";\n/** The domain the updater key signs in: a manifest, never anything else. */\nexport const MANIFEST_DOMAIN = \"scopebond:agent-release/v1\\n\";\n/** The publisher an installer must be signed by. */\nexport const PUBLISHER = /(^|,\\s*)O=Avouro LLC(,|$)/;\n\n/** Set by the single executable's build from the updater's public key (base64 SPKI); absent from npm builds. */\ndeclare const __SCOPEBOND_UPDATER_KEY__: string | undefined;\n\nexport function updaterPublicKey(): string | null {\n return typeof __SCOPEBOND_UPDATER_KEY__ === \"string\" && __SCOPEBOND_UPDATER_KEY__ ? __SCOPEBOND_UPDATER_KEY__ : null;\n}\n\nexport type InstallKind = \"npm\" | \"per-user\" | \"per-machine\";\n\n/** How this agent was installed: from npm, by the installer for this user, or for every user (Program Files). */\nexport function installKind(execPath = process.execPath, single = isSingleExecutable(), programFiles = process.env.ProgramFiles ?? \"C:\\\\Program Files\"): InstallKind {\n if (!single) return \"npm\";\n // The folder without trailing separators (a loop, not a regular expression, on a value from the environment).\n let folder = programFiles.toLowerCase();\n while (folder.endsWith(\"\\\\\") || folder.endsWith(\"/\")) folder = folder.slice(0, -1);\n return execPath.toLowerCase().startsWith(`${folder}\\\\`) ? \"per-machine\" : \"per-user\";\n}\n\nexport interface ReleaseManifest { type: \"scopebond:agent-release\"; version: string; files: Array<{ name: string; sha256: string; size: number }> }\n\n/** The manifest when its signature is the updater key's over exactly these bytes, else null. */\nexport function verifyManifest(text: string, signatureB64: string, publicKeyB64: string): ReleaseManifest | null {\n try {\n const key = createPublicKey({ key: Buffer.from(publicKeyB64, \"base64\"), format: \"der\", type: \"spki\" });\n if (!verify(null, Buffer.from(MANIFEST_DOMAIN + text, \"utf8\"), key, Buffer.from(signatureB64.trim(), \"base64\"))) return null;\n const manifest = JSON.parse(text) as ReleaseManifest;\n if (manifest.type !== \"scopebond:agent-release\" || typeof manifest.version !== \"string\" || !Array.isArray(manifest.files)) return null;\n return manifest;\n } catch { return null; }\n}\n\nexport const installerName = (version: string) => `scopebond-agent-${version}-x64.msi`;\n\n/** The Authenticode check: Windows' own verdict on the file, and the signer's subject. */\nexport function authenticode(file: string): { valid: boolean; subject: string } {\n const script = `$s = Get-AuthenticodeSignature -LiteralPath $env:SB_FILE; [Console]::Out.Write((@{ status = \"$($s.Status)\"; subject = \"$($s.SignerCertificate.Subject)\" } | ConvertTo-Json -Compress))`;\n const run = spawnSync(\"powershell.exe\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", script], { encoding: \"utf8\", env: { ...process.env, SB_FILE: file }, windowsHide: true, timeout: 60_000 });\n try { const r = JSON.parse(run.stdout) as { status: string; subject: string }; return { valid: r.status === \"Valid\", subject: r.subject }; }\n catch { return { valid: false, subject: \"\" }; }\n}\n\nexport interface VerifiedInstaller { ok: true; path: string; version: string }\nexport interface Refused { ok: false; reason: string }\n\n/** Download the installer for `version` and check it three ways. Nothing is kept unless every check passes. */\nexport async function fetchVerifiedInstaller(version: string, o: {\n dir: string; fetchImpl?: typeof fetch; publicKey?: string | null; check?: (file: string) => { valid: boolean; subject: string }; base?: string;\n}): Promise<VerifiedInstaller | Refused> {\n const publicKey = o.publicKey === undefined ? updaterPublicKey() : o.publicKey;\n if (!publicKey) return { ok: false, reason: \"this build has no updater key\" };\n if (!/^\\d+\\.\\d+\\.\\d+$/.test(version)) return { ok: false, reason: \"not a version\" };\n const fetchImpl = o.fetchImpl ?? fetch;\n const base = `${o.base ?? RELEASE_BASE}/agent-native-v${version}`;\n const get = async (name: string): Promise<Buffer | null> => {\n try {\n const res = await fetchImpl(`${base}/${name}`, { redirect: \"follow\", signal: AbortSignal.timeout(5 * 60_000) });\n return res.ok ? Buffer.from(await res.arrayBuffer()) : null;\n } catch { return null; }\n };\n const [text, signature] = await Promise.all([get(`scopebond-agent-${version}.manifest.json`), get(`scopebond-agent-${version}.manifest.json.sig`)]);\n if (!text || !signature) return { ok: false, reason: \"the release has no signed manifest\" };\n const manifest = verifyManifest(text.toString(\"utf8\"), signature.toString(\"utf8\"), publicKey);\n if (!manifest) return { ok: false, reason: \"the release manifest is not signed by the updater key\" };\n if (manifest.version !== version) return { ok: false, reason: `the manifest is for ${manifest.version}, not ${version}` };\n const entry = manifest.files.find((f) => f.name === installerName(version));\n if (!entry) return { ok: false, reason: \"the manifest names no installer\" };\n const bytes = await get(entry.name);\n if (!bytes) return { ok: false, reason: \"the installer could not be downloaded\" };\n if (bytes.length !== entry.size || createHash(\"sha256\").update(bytes).digest(\"hex\") !== entry.sha256) return { ok: false, reason: \"the installer is not the one the manifest names\" };\n const folder = join(o.dir, \"updates\");\n mkdirSync(folder, { recursive: true });\n const path = join(folder, entry.name);\n writeFileSync(path, bytes);\n const signed = (o.check ?? authenticode)(path);\n if (!signed.valid) return { ok: false, reason: \"the installer's signature is not valid\" };\n if (!PUBLISHER.test(signed.subject)) return { ok: false, reason: `the installer is signed by ${signed.subject || \"someone else\"}, not Avouro LLC` };\n return { ok: true, path, version };\n}\n\n/** Install it once this agent has exited, then start the agent again through autostart's launcher. A detached helper does it,\n * because the installer replaces this very file. */\nexport function installAfterExit(installer: string, pid: number, launcher: string | null): void {\n const script = [\n \"$ErrorActionPreference = 'SilentlyContinue'\",\n \"Wait-Process -Id ([int]$env:SB_PID) -Timeout 60\",\n \"Start-Process -FilePath msiexec.exe -ArgumentList @('/i', ('\\\"' + $env:SB_MSI + '\\\"'), '/qn', '/norestart') -Wait\",\n \"if ($env:SB_LAUNCHER) { Start-Process -FilePath cmd.exe -ArgumentList @('/d', '/s', '/c', ('\\\"\\\"' + $env:SB_LAUNCHER + '\\\"\\\"')) -WindowStyle Hidden }\",\n ].join(\"; \");\n const child = spawn(\"powershell.exe\", [\"-NoProfile\", \"-NonInteractive\", \"-WindowStyle\", \"Hidden\", \"-Command\", script], {\n detached: true, stdio: \"ignore\", windowsHide: true, env: { ...process.env, SB_PID: String(pid), SB_MSI: installer, SB_LAUNCHER: launcher ?? \"\" },\n });\n child.on(\"error\", () => { /* the next sign-in starts the agent */ });\n child.unref();\n}\n"]}
package/dist/prompt.d.ts CHANGED
@@ -6,11 +6,21 @@ export interface OverrideQuestion {
6
6
  reason_min: number;
7
7
  lasts: string;
8
8
  timeout_ms: number;
9
+ /** D144: override = "Block, person may allow"; ask = "Block, person may ask" (only Ask an admin). */
10
+ mode: "override" | "ask";
11
+ /** Which choices the workspace allows: allow (once), fifteen (for 15 minutes), always (an allowance), ask (Ask an admin). */
12
+ offers: {
13
+ allow: boolean;
14
+ always: boolean;
15
+ ask: boolean;
16
+ fifteen?: boolean;
17
+ };
9
18
  }
10
19
  export interface OverrideAnswer {
11
- decision: "allow" | "deny" | "unavailable";
20
+ decision: "allow" | "deny" | "unavailable" | "ask";
12
21
  reason?: string;
13
22
  os_user?: string;
23
+ lasts?: "once" | "15m" | "always";
14
24
  }
15
25
  export type Prompter = (question: OverrideQuestion) => Promise<OverrideAnswer>;
16
26
  /** Check a request from the local channel. Anything malformed is refused before a window opens. */
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AACD,MAAM,WAAW,cAAc;IAAG,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE;AACjH,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,gBAAgB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAK/E,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAWnE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAExD;AAiBD,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CA2CzD;AA0CD,iHAAiH;AACjH,eAAO,MAAM,cAAc,EAAE,QAQ5B,CAAC;AAEF,uHAAuH;AACvH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAQvD"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,qGAAqG;IACrG,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC;IACzB,6HAA6H;IAC7H,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9E;AACD,MAAM,WAAW,cAAc;IAAG,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,aAAa,GAAG,KAAK,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAA;CAAE;AAC5J,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,gBAAgB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAK/E,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAmBnE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAIxD;AAiBD,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAmDzD;AAoDD,iHAAiH;AACjH,eAAO,MAAM,cAAc,EAAE,QAQ5B,CAAC;AAEF,uHAAuH;AACvH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAQvD"}