@scopebond/agent 0.5.6 → 0.6.1

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 (61) hide show
  1. package/dist/agent.d.ts +6 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +10 -3
  4. package/dist/agent.js.map +1 -1
  5. package/dist/allowance-sender.js +10 -4
  6. package/dist/allowance-sender.js.map +1 -1
  7. package/dist/apps-entry.d.ts +46 -0
  8. package/dist/apps-entry.d.ts.map +1 -0
  9. package/dist/apps-entry.js +118 -0
  10. package/dist/apps-entry.js.map +1 -0
  11. package/dist/autostart.d.ts +11 -5
  12. package/dist/autostart.d.ts.map +1 -1
  13. package/dist/autostart.js +60 -6
  14. package/dist/autostart.js.map +1 -1
  15. package/dist/awake.d.ts +31 -0
  16. package/dist/awake.d.ts.map +1 -0
  17. package/dist/awake.js +57 -0
  18. package/dist/awake.js.map +1 -0
  19. package/dist/cli-main.d.ts.map +1 -1
  20. package/dist/cli-main.js +28 -1
  21. package/dist/cli-main.js.map +1 -1
  22. package/dist/index.d.ts +12 -7
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +8 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/ipc.d.ts +9 -4
  27. package/dist/ipc.d.ts.map +1 -1
  28. package/dist/ipc.js +15 -9
  29. package/dist/ipc.js.map +1 -1
  30. package/dist/log-file.js.map +1 -1
  31. package/dist/native-update.d.ts +83 -7
  32. package/dist/native-update.d.ts.map +1 -1
  33. package/dist/native-update.js +230 -33
  34. package/dist/native-update.js.map +1 -1
  35. package/dist/prompt.d.ts.map +1 -1
  36. package/dist/prompt.js +1 -0
  37. package/dist/prompt.js.map +1 -1
  38. package/dist/selfcheck.d.ts.map +1 -1
  39. package/dist/selfcheck.js +4 -1
  40. package/dist/selfcheck.js.map +1 -1
  41. package/dist/service.d.ts +20 -3
  42. package/dist/service.d.ts.map +1 -1
  43. package/dist/service.js +141 -44
  44. package/dist/service.js.map +1 -1
  45. package/dist/setup.d.ts +12 -0
  46. package/dist/setup.d.ts.map +1 -1
  47. package/dist/setup.js +42 -13
  48. package/dist/setup.js.map +1 -1
  49. package/dist/tray-model.d.ts +2 -0
  50. package/dist/tray-model.d.ts.map +1 -1
  51. package/dist/tray-model.js +2 -0
  52. package/dist/tray-model.js.map +1 -1
  53. package/dist/update.d.ts +56 -12
  54. package/dist/update.d.ts.map +1 -1
  55. package/dist/update.js +209 -38
  56. package/dist/update.js.map +1 -1
  57. package/dist/upkeep.d.ts +16 -0
  58. package/dist/upkeep.d.ts.map +1 -0
  59. package/dist/upkeep.js +55 -0
  60. package/dist/upkeep.js.map +1 -0
  61. package/package.json +4 -4
package/dist/ipc.js CHANGED
@@ -1,7 +1,8 @@
1
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.
2
+ // socket in a folder only this user can open. npm installs also listen on 127.0.0.1 at a random port, because the
3
+ // PowerShell tray and hooks before the pipe use it (SCOPEBOND_AGENT_LOOPBACK=0 turns it off); the signed install with the
4
+ // native tray does not (its tray and its hook use the pipe). The pipe or socket, the port, the token and the process id
5
+ // are written to agent.json in the Scopebond home, which is how the CLI, the hook's override window and the tray find it.
5
6
  // 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
7
  // or opened by another user. Every request must carry the token either way.
7
8
  import { createServer } from "node:http";
@@ -34,8 +35,10 @@ export function readEndpoint(dir) {
34
35
  return null;
35
36
  }
36
37
  }
37
- /** Start the control server. `routes` maps "METHOD /path" to a handler. */
38
- export async function startControl(dir, version, routes) {
38
+ /** Start the control server. `routes` maps "METHOD /path" to a handler. `loopback`: also listen on 127.0.0.1 (default:
39
+ * unless SCOPEBOND_AGENT_LOOPBACK=0); it is used anyway when the pipe or socket could not be made, or nothing could reach
40
+ * the agent. */
41
+ export async function startControl(dir, version, routes, options = {}) {
39
42
  const token = randomBytes(24).toString("base64url");
40
43
  const expected = Buffer.from(token);
41
44
  const handle = async (req, res) => {
@@ -59,9 +62,11 @@ export async function startControl(dir, version, routes) {
59
62
  send(500, { error: error.message.slice(0, 200) });
60
63
  }
61
64
  };
65
+ // A caller that drops the connection mid-body rejects the read; that ends the request, never the agent.
66
+ const listener = (req, res) => { handle(req, res).catch(() => { res.destroy(); }); };
62
67
  // The pipe or socket first. A Unix socket's folder is created for this user only, and the socket itself is too.
63
68
  const socketPath = localSocketPath(dir);
64
- const local = createServer(handle);
69
+ const local = createServer(listener);
65
70
  let socket = socketPath;
66
71
  try {
67
72
  if (process.platform !== "win32") {
@@ -76,9 +81,10 @@ export async function startControl(dir, version, routes) {
76
81
  catch {
77
82
  socket = null;
78
83
  }
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);
84
+ // Loopback where something still needs it (the PowerShell tray, older hooks), unless turned off; always when the pipe
85
+ // could not be made.
86
+ const loopback = (options.loopback ?? process.env.SCOPEBOND_AGENT_LOOPBACK !== "0") || socket === null;
87
+ const server = createServer(listener);
82
88
  if (loopback)
83
89
  await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => resolve()); });
84
90
  const address = loopback ? server.address() : null;
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: 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; charset=utf-8\", \"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"]}
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. npm installs also listen on 127.0.0.1 at a random port, because the\n// PowerShell tray and hooks before the pipe use it (SCOPEBOND_AGENT_LOOPBACK=0 turns it off); the signed install with the\n// native tray does not (its tray and its hook use the pipe). The pipe or socket, the port, the token and the process id\n// are 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\n/** Returns the answer, or a promise of it. */\nexport type Handler = (body: 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. `loopback`: also listen on 127.0.0.1 (default:\n * unless SCOPEBOND_AGENT_LOOPBACK=0); it is used anyway when the pipe or socket could not be made, or nothing could reach\n * the agent. */\nexport async function startControl(dir: string, version: string, routes: Record<string, Handler>, options: { loopback?: boolean } = {}): 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; charset=utf-8\", \"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 // A caller that drops the connection mid-body rejects the read; that ends the request, never the agent.\n const listener = (req: IncomingMessage, res: ServerResponse) => { handle(req, res).catch(() => { res.destroy(); }); };\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(listener);\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 where something still needs it (the PowerShell tray, older hooks), unless turned off; always when the pipe\n // could not be made.\n const loopback = (options.loopback ?? process.env.SCOPEBOND_AGENT_LOOPBACK !== \"0\") || socket === null;\n const server = createServer(listener);\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> {\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"]}
@@ -1 +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"]}
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,CAAC,CAAC;IACL,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 });\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"]}
@@ -4,22 +4,40 @@ export declare const RELEASE_BASE = "https://github.com/avouro-com/scopebond/rel
4
4
  export declare const MANIFEST_DOMAIN = "scopebond:agent-release/v1\n";
5
5
  /** The publisher an installer must be signed by. */
6
6
  export declare const PUBLISHER: RegExp;
7
- export declare function updaterPublicKey(): string | null;
7
+ /** One trusted updater key: its id (the first 16 hex digits of the SHA-256 of its SPKI bytes), its public half (base64
8
+ * SPKI), and the last day it may sign (an ISO date, inclusive), or null for no end. */
9
+ export interface UpdaterKey {
10
+ kid: string;
11
+ key: string;
12
+ not_after: string | null;
13
+ }
14
+ /** A public key's id: the first 16 hex digits of the SHA-256 of its SPKI bytes. */
15
+ export declare function keyId(publicKeyB64: string): string;
16
+ /** The updater keys this build trusts (empty when it has none). */
17
+ export declare function updaterKeys(): UpdaterKey[];
8
18
  export type InstallKind = "npm" | "per-user" | "per-machine";
9
19
  /** How this agent was installed: from npm, by the installer for this user, or for every user (Program Files). */
10
20
  export declare function installKind(execPath?: string, single?: boolean, programFiles?: string): InstallKind;
11
21
  export interface ReleaseManifest {
12
22
  type: "scopebond:agent-release";
13
23
  version: string;
24
+ kid?: string;
14
25
  files: Array<{
15
26
  name: string;
16
27
  sha256: string;
17
28
  size: number;
18
29
  }>;
19
30
  }
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;
31
+ /** The manifest when its signature is one trusted, in-date updater key's over exactly these bytes, else null. A manifest
32
+ * that names its key id (`kid`) is checked with that key only. */
33
+ export declare function verifyManifest(text: string, signatureB64: string, keys: string | UpdaterKey[], now?: number): ReleaseManifest | null;
22
34
  export declare const installerName: (version: string) => string;
35
+ /** The environment for Windows PowerShell (powershell.exe, 5.1): this one's, without `PSModulePath`, plus `extra`. An
36
+ * agent started from PowerShell 7 inherits its module path, and Windows PowerShell then cannot load its own security
37
+ * module (Get-AuthenticodeSignature), so every check would fail; without the variable it uses its own default. */
38
+ export declare function powershellEnv(extra: Record<string, string>, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
39
+ /** A Windows tool by its full path under the system folder, never by a name looked up on PATH. */
40
+ export declare function windowsTool(name: "powershell" | "msiexec" | "cmd"): string;
23
41
  /** The Authenticode check: Windows' own verdict on the file, and the signer's subject. */
24
42
  export declare function authenticode(file: string): {
25
43
  valid: boolean;
@@ -29,6 +47,8 @@ export interface VerifiedInstaller {
29
47
  ok: true;
30
48
  path: string;
31
49
  version: string;
50
+ sha256: string;
51
+ size: number;
32
52
  }
33
53
  export interface Refused {
34
54
  ok: false;
@@ -38,14 +58,70 @@ export interface Refused {
38
58
  export declare function fetchVerifiedInstaller(version: string, o: {
39
59
  dir: string;
40
60
  fetchImpl?: typeof fetch;
41
- publicKey?: string | null;
61
+ publicKey?: string | UpdaterKey[] | null;
42
62
  check?: (file: string) => {
43
63
  valid: boolean;
44
64
  subject: string;
45
65
  };
46
66
  base?: string;
67
+ now?: number;
47
68
  }): 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;
69
+ /** What the helper that installs after this agent exits records, beside the installer. */
70
+ export declare const INSTALL_RESULT = "install-result.json";
71
+ export interface InstallResult {
72
+ at: string;
73
+ version: string;
74
+ installed: boolean;
75
+ reason: string | null;
76
+ exit_code: number | null;
77
+ }
78
+ /** The helper's script. It is constant text: every value (the installer, its digest and size, the publisher rule, where to
79
+ * record the outcome) reaches it through environment variables, never through the script itself.
80
+ *
81
+ * Between the download and the install the agent exits and up to a minute passes, so the helper checks the installer again
82
+ * right before it runs msiexec, the same way the agent did: its size and SHA-256 are the signed manifest's, its Authenticode
83
+ * signature is valid and its signer matches the same anchored publisher rule (PUBLISHER). It holds the file open, denying
84
+ * writes and deletes, from that check until msiexec has finished, so the file checked is the file installed. Any mismatch
85
+ * installs nothing and records why in INSTALL_RESULT; the agent is started again either way. */
86
+ export declare const INSTALL_HELPER_SCRIPT: string;
87
+ /** The helper's script (INSTALL_HELPER_SCRIPT): stop the native tray, wait for this agent to exit, check the installer
88
+ * again, install, then start the tray again (or the agent through autostart's launcher). */
89
+ export declare function installAfterExitScript(): string;
90
+ /** The native Scopebond tray the installer puts beside the single executable (`scopebond-tray.exe`), when it is there. It
91
+ * starts the agent at sign-in and keeps it running, so the agent leaves the tray to it. */
92
+ export declare function nativeTrayPath(execPath?: string, single?: boolean, exists?: (path: string) => boolean): string | null;
93
+ /** The helper's program, arguments and environment values (pure, so it can be tested). */
94
+ export declare function installHelper(installer: {
95
+ path: string;
96
+ version: string;
97
+ sha256: string;
98
+ size: number;
99
+ }, pid: number, launcher: string | null, o?: {
100
+ msiexec?: string;
101
+ publisher?: RegExp;
102
+ tray?: string | null;
103
+ }): {
104
+ program: string;
105
+ args: string[];
106
+ env: Record<string, string>;
107
+ };
108
+ /** Install it once this agent has exited, then start the tray again (or the agent through autostart's launcher). A detached
109
+ * helper does it, because the installer replaces this very file; it checks the installer again first (INSTALL_HELPER_SCRIPT). */
110
+ export declare function installAfterExit(installer: {
111
+ path: string;
112
+ version: string;
113
+ sha256: string;
114
+ size: number;
115
+ }, pid: number, launcher: string | null, tray?: string | null): void;
116
+ /** Whether the installer on disk is still the verified one: same size and SHA-256, and still validly signed by Avouro LLC. */
117
+ export declare function installerUnchanged(installer: Pick<VerifiedInstaller, "path" | "sha256" | "size">, check?: (file: string) => {
118
+ valid: boolean;
119
+ subject: string;
120
+ }): boolean;
121
+ /** The helper's check alone: exit 0 when $env:SB_MSI is still the verified installer, 3 when it is not. */
122
+ export declare function installerRecheckScript(): string;
123
+ /** The helper script that installs after this agent exits (INSTALL_HELPER_SCRIPT). */
124
+ export declare function installHelperScript(): string;
125
+ /** What the last install helper recorded, once: the file is removed when read. Null when there is nothing. */
126
+ export declare function takeInstallResult(dir: string): InstallResult | null;
51
127
  //# sourceMappingURL=native-update.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"native-update.d.ts","sourceRoot":"","sources":["../src/native-update.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,16 @@
1
1
  // Updates for the signed Windows install (the single executable from the installer). npm installs keep updating with npm.
2
2
  //
3
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.
4
+ // with an updater key (a separate Ed25519 key; the public halves it trusts are built into this program, each with a key id
5
+ // and an optional last day of use, so the key can be rotated), the installer's SHA-256 and size are the manifest's, and
6
+ // the installer's Authenticode signature is valid and names Avouro LLC. The helper that runs msiexec after this agent has
7
+ // exited checks the digest, size and signature again from a handle that keeps the file from being changed, and only then
8
+ // starts msiexec. Anything else and nothing is installed: the agent says the update could not be verified. An install for
9
+ // every user (Program Files) never updates itself; the organisation that deployed it updates it.
8
10
  import { createHash, createPublicKey, verify } from "node:crypto";
9
11
  import { spawn, spawnSync } from "node:child_process";
10
- import { mkdirSync, writeFileSync } from "node:fs";
11
- import { join } from "node:path";
12
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
13
+ import { dirname, join, win32 } from "node:path";
12
14
  import { isSingleExecutable } from "@scopebond/hook";
13
15
  /** Where the signed releases are published. */
14
16
  export const RELEASE_BASE = "https://github.com/avouro-com/scopebond/releases/download";
@@ -16,8 +18,39 @@ export const RELEASE_BASE = "https://github.com/avouro-com/scopebond/releases/do
16
18
  export const MANIFEST_DOMAIN = "scopebond:agent-release/v1\n";
17
19
  /** The publisher an installer must be signed by. */
18
20
  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
+ /** A public key's id: the first 16 hex digits of the SHA-256 of its SPKI bytes. */
22
+ export function keyId(publicKeyB64) {
23
+ return createHash("sha256").update(Buffer.from(publicKeyB64, "base64")).digest("hex").slice(0, 16);
24
+ }
25
+ /** The updater keys this build trusts (empty when it has none). */
26
+ export function updaterKeys() {
27
+ if (typeof __SCOPEBOND_UPDATER_KEYS__ !== "string" || !__SCOPEBOND_UPDATER_KEYS__)
28
+ return [];
29
+ try {
30
+ return normalizeKeys(JSON.parse(__SCOPEBOND_UPDATER_KEYS__));
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ }
36
+ function normalizeKeys(value) {
37
+ if (typeof value === "string")
38
+ return value ? [{ kid: keyId(value), key: value, not_after: null }] : [];
39
+ if (!Array.isArray(value))
40
+ return [];
41
+ return value.flatMap((k) => {
42
+ const e = k;
43
+ if (!e || typeof e.key !== "string" || !e.key)
44
+ return [];
45
+ return [{ kid: typeof e.kid === "string" && e.kid ? e.kid : keyId(e.key), key: e.key, not_after: typeof e.not_after === "string" ? e.not_after : null }];
46
+ });
47
+ }
48
+ /** Whether a key may still sign at `now`: no end, or `now` is on or before its last day. An unreadable date counts as past. */
49
+ function inDate(key, now) {
50
+ if (key.not_after === null)
51
+ return true;
52
+ const end = /^\d{4}-\d{2}-\d{2}$/.test(key.not_after) ? Date.parse(`${key.not_after}T23:59:59.999Z`) : Date.parse(key.not_after);
53
+ return Number.isFinite(end) && now <= end;
21
54
  }
22
55
  /** How this agent was installed: from npm, by the installer for this user, or for every user (Program Files). */
23
56
  export function installKind(execPath = process.execPath, single = isSingleExecutable(), programFiles = process.env.ProgramFiles ?? "C:\\Program Files") {
@@ -29,26 +62,58 @@ export function installKind(execPath = process.execPath, single = isSingleExecut
29
62
  folder = folder.slice(0, -1);
30
63
  return execPath.toLowerCase().startsWith(`${folder}\\`) ? "per-machine" : "per-user";
31
64
  }
32
- /** The manifest when its signature is the updater key's over exactly these bytes, else null. */
33
- export function verifyManifest(text, signatureB64, publicKeyB64) {
65
+ /** The manifest when its signature is one trusted, in-date updater key's over exactly these bytes, else null. A manifest
66
+ * that names its key id (`kid`) is checked with that key only. */
67
+ export function verifyManifest(text, signatureB64, keys, now = Date.now()) {
34
68
  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")))
69
+ const ring = normalizeKeys(keys).filter((k) => inDate(k, now));
70
+ if (!ring.length)
71
+ return null;
72
+ const parsed = JSON.parse(text);
73
+ const named = typeof parsed?.kid === "string" ? parsed.kid : null;
74
+ const candidates = named === null ? ring : ring.filter((k) => k.kid === named);
75
+ const message = Buffer.from(MANIFEST_DOMAIN + text, "utf8");
76
+ const signature = Buffer.from(signatureB64.trim(), "base64");
77
+ const signed = candidates.some((k) => {
78
+ try {
79
+ return verify(null, message, createPublicKey({ key: Buffer.from(k.key, "base64"), format: "der", type: "spki" }), signature);
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ });
85
+ if (!signed)
37
86
  return null;
38
- const manifest = JSON.parse(text);
39
- if (manifest.type !== "scopebond:agent-release" || typeof manifest.version !== "string" || !Array.isArray(manifest.files))
87
+ if (parsed.type !== "scopebond:agent-release" || typeof parsed.version !== "string" || !Array.isArray(parsed.files))
40
88
  return null;
41
- return manifest;
89
+ return parsed;
42
90
  }
43
91
  catch {
44
92
  return null;
45
93
  }
46
94
  }
47
95
  export const installerName = (version) => `scopebond-agent-${version}-x64.msi`;
96
+ /** The environment for Windows PowerShell (powershell.exe, 5.1): this one's, without `PSModulePath`, plus `extra`. An
97
+ * agent started from PowerShell 7 inherits its module path, and Windows PowerShell then cannot load its own security
98
+ * module (Get-AuthenticodeSignature), so every check would fail; without the variable it uses its own default. */
99
+ export function powershellEnv(extra, base = process.env) {
100
+ const env = {};
101
+ for (const [key, value] of Object.entries(base))
102
+ if (key.toLowerCase() !== "psmodulepath")
103
+ env[key] = value;
104
+ return { ...env, ...extra };
105
+ }
106
+ /** A Windows tool by its full path under the system folder, never by a name looked up on PATH. */
107
+ export function windowsTool(name) {
108
+ const root = process.env.SystemRoot || process.env.windir || "C:\\Windows";
109
+ if (name === "powershell")
110
+ return win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
111
+ return win32.join(root, "System32", `${name}.exe`);
112
+ }
48
113
  /** The Authenticode check: Windows' own verdict on the file, and the signer's subject. */
49
114
  export function authenticode(file) {
50
115
  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 });
116
+ const run = spawnSync(windowsTool("powershell"), ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8", env: powershellEnv({ SB_FILE: file }), windowsHide: true, timeout: 60_000 });
52
117
  try {
53
118
  const r = JSON.parse(run.stdout);
54
119
  return { valid: r.status === "Valid", subject: r.subject };
@@ -59,8 +124,8 @@ export function authenticode(file) {
59
124
  }
60
125
  /** Download the installer for `version` and check it three ways. Nothing is kept unless every check passes. */
61
126
  export async function fetchVerifiedInstaller(version, o) {
62
- const publicKey = o.publicKey === undefined ? updaterPublicKey() : o.publicKey;
63
- if (!publicKey)
127
+ const keys = o.publicKey === undefined ? updaterKeys() : o.publicKey === null ? [] : normalizeKeys(o.publicKey);
128
+ if (!keys.length)
64
129
  return { ok: false, reason: "this build has no updater key" };
65
130
  if (!/^\d+\.\d+\.\d+$/.test(version))
66
131
  return { ok: false, reason: "not a version" };
@@ -78,7 +143,7 @@ export async function fetchVerifiedInstaller(version, o) {
78
143
  const [text, signature] = await Promise.all([get(`scopebond-agent-${version}.manifest.json`), get(`scopebond-agent-${version}.manifest.json.sig`)]);
79
144
  if (!text || !signature)
80
145
  return { ok: false, reason: "the release has no signed manifest" };
81
- const manifest = verifyManifest(text.toString("utf8"), signature.toString("utf8"), publicKey);
146
+ const manifest = verifyManifest(text.toString("utf8"), signature.toString("utf8"), keys, o.now);
82
147
  if (!manifest)
83
148
  return { ok: false, reason: "the release manifest is not signed by the updater key" };
84
149
  if (manifest.version !== version)
@@ -100,21 +165,153 @@ export async function fetchVerifiedInstaller(version, o) {
100
165
  return { ok: false, reason: "the installer's signature is not valid" };
101
166
  if (!PUBLISHER.test(signed.subject))
102
167
  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
- });
168
+ return { ok: true, path, version, sha256: entry.sha256, size: entry.size };
169
+ }
170
+ /** What the helper that installs after this agent exits records, beside the installer. */
171
+ export const INSTALL_RESULT = "install-result.json";
172
+ /** The helper's script. It is constant text: every value (the installer, its digest and size, the publisher rule, where to
173
+ * record the outcome) reaches it through environment variables, never through the script itself.
174
+ *
175
+ * Between the download and the install the agent exits and up to a minute passes, so the helper checks the installer again
176
+ * right before it runs msiexec, the same way the agent did: its size and SHA-256 are the signed manifest's, its Authenticode
177
+ * signature is valid and its signer matches the same anchored publisher rule (PUBLISHER). It holds the file open, denying
178
+ * writes and deletes, from that check until msiexec has finished, so the file checked is the file installed. Any mismatch
179
+ * installs nothing and records why in INSTALL_RESULT; the agent is started again either way. */
180
+ export const INSTALL_HELPER_SCRIPT = [
181
+ "$ErrorActionPreference = 'Stop'",
182
+ "function Write-Result([bool] $installed, $reason, $code) {",
183
+ " try {",
184
+ " $json = [ordered]@{ at = [DateTime]::UtcNow.ToString('o'); version = $env:SB_VERSION; installed = $installed; reason = $reason; exit_code = $code } | ConvertTo-Json -Compress",
185
+ " [IO.File]::WriteAllText($env:SB_RESULT, $json, (New-Object Text.UTF8Encoding $false))",
186
+ " } catch { }",
187
+ "}",
188
+ // The native tray first: it would start the old agent again, and the installer replaces its file. Only this install's.
189
+ "if ($env:SB_TRAY) { try { Get-Process -Name scopebond-tray -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $env:SB_TRAY } | Stop-Process -Force -ErrorAction SilentlyContinue } catch { } }",
190
+ "try { Wait-Process -Id ([int]$env:SB_PID) -Timeout 60 -ErrorAction SilentlyContinue } catch { }",
191
+ "$lock = $null",
192
+ "$reason = $null",
193
+ "try {",
194
+ " $lock = New-Object IO.FileStream($env:SB_MSI, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)",
195
+ " if ($lock.Length -ne [long]$env:SB_SIZE) { $reason = \"the installer is $($lock.Length) bytes, not the manifest's $($env:SB_SIZE)\" }",
196
+ " else {",
197
+ " $sha = [Security.Cryptography.SHA256]::Create()",
198
+ " $digest = ([BitConverter]::ToString($sha.ComputeHash($lock)) -replace '-', '').ToLowerInvariant()",
199
+ " if ($digest -cne $env:SB_SHA256.ToLowerInvariant()) { $reason = 'the installer changed after it was checked: its SHA-256 is not the manifest''s' }",
200
+ " else {",
201
+ " $sig = Get-AuthenticodeSignature -LiteralPath $env:SB_MSI",
202
+ " $subject = \"$($sig.SignerCertificate.Subject)\"",
203
+ " if (\"$($sig.Status)\" -ne 'Valid') { $reason = \"the installer's signature is not valid ($($sig.Status))\" }",
204
+ " elseif (-not ($subject -cmatch $env:SB_PUBLISHER)) { $reason = \"the installer is signed by $subject, not the expected publisher\" }",
205
+ " }",
206
+ " }",
207
+ "} catch { $reason = \"the installer could not be checked: $($_.Exception.Message)\" }",
208
+ "try {",
209
+ " if ($reason) { Write-Result $false $reason $null }",
210
+ " else {",
211
+ " try {",
212
+ // Windows' own msiexec by its full path (SB_MSIEXEC only replaces it in tests).
213
+ " $msiexec = if ($env:SB_MSIEXEC) { $env:SB_MSIEXEC } else { Join-Path $env:SystemRoot 'System32\\msiexec.exe' }",
214
+ " $p = Start-Process -FilePath $msiexec -ArgumentList @('/i', ('\"' + $env:SB_MSI + '\"'), '/qn', '/norestart') -Wait -PassThru",
215
+ " $code = [int]$p.ExitCode",
216
+ " if ($code -eq 0 -or $code -eq 3010) { Write-Result $true $null $code } else { Write-Result $false \"msiexec ended with exit code $code\" $code }",
217
+ " } catch { Write-Result $false \"msiexec could not start: $($_.Exception.Message)\" $null }",
218
+ " }",
219
+ "} finally { if ($lock) { $lock.Dispose() } }",
220
+ // Installed or not, the tray starts again (it starts the agent), or else the agent through autostart's launcher.
221
+ "if ($env:SB_TRAY) { Start-Process -FilePath $env:SB_TRAY } elseif ($env:SB_LAUNCHER) { Start-Process -FilePath (Join-Path $env:SystemRoot 'System32\\cmd.exe') -ArgumentList @('/d', '/s', '/c', ('\"\"' + $env:SB_LAUNCHER + '\"\"')) -WindowStyle Hidden }",
222
+ ].join("\n");
223
+ /** The helper's script (INSTALL_HELPER_SCRIPT): stop the native tray, wait for this agent to exit, check the installer
224
+ * again, install, then start the tray again (or the agent through autostart's launcher). */
225
+ export function installAfterExitScript() {
226
+ return INSTALL_HELPER_SCRIPT;
227
+ }
228
+ /** The native Scopebond tray the installer puts beside the single executable (`scopebond-tray.exe`), when it is there. It
229
+ * starts the agent at sign-in and keeps it running, so the agent leaves the tray to it. */
230
+ export function nativeTrayPath(execPath = process.execPath, single = isSingleExecutable(), exists = existsSync) {
231
+ if (!single)
232
+ return null;
233
+ const tray = win32.join(win32.dirname(execPath), "scopebond-tray.exe");
234
+ return exists(tray) ? tray : null;
235
+ }
236
+ /** The helper's program, arguments and environment values (pure, so it can be tested). */
237
+ export function installHelper(installer, pid, launcher, o = {}) {
238
+ return {
239
+ program: windowsTool("powershell"),
240
+ args: ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", INSTALL_HELPER_SCRIPT],
241
+ env: {
242
+ SB_PID: String(pid),
243
+ SB_MSI: installer.path,
244
+ SB_VERSION: installer.version,
245
+ SB_SHA256: installer.sha256,
246
+ SB_SIZE: String(installer.size),
247
+ SB_PUBLISHER: (o.publisher ?? PUBLISHER).source,
248
+ SB_RESULT: join(dirname(installer.path), INSTALL_RESULT),
249
+ SB_MSIEXEC: o.msiexec ?? windowsTool("msiexec"),
250
+ SB_LAUNCHER: launcher ?? "",
251
+ SB_TRAY: o.tray ?? "",
252
+ },
253
+ };
254
+ }
255
+ /** Install it once this agent has exited, then start the tray again (or the agent through autostart's launcher). A detached
256
+ * helper does it, because the installer replaces this very file; it checks the installer again first (INSTALL_HELPER_SCRIPT). */
257
+ export function installAfterExit(installer, pid, launcher, tray = null) {
258
+ const helper = installHelper(installer, pid, launcher, { tray });
259
+ const child = spawn(helper.program, helper.args, { detached: true, stdio: "ignore", windowsHide: true, env: powershellEnv(helper.env) });
117
260
  child.on("error", () => { });
118
261
  child.unref();
119
262
  }
263
+ /** Whether the installer on disk is still the verified one: same size and SHA-256, and still validly signed by Avouro LLC. */
264
+ export function installerUnchanged(installer, check = authenticode) {
265
+ try {
266
+ const bytes = readFileSync(installer.path);
267
+ if (bytes.length !== installer.size || createHash("sha256").update(bytes).digest("hex") !== installer.sha256)
268
+ return false;
269
+ const signed = check(installer.path);
270
+ return signed.valid && PUBLISHER.test(signed.subject);
271
+ }
272
+ catch {
273
+ return false;
274
+ }
275
+ }
276
+ /** PowerShell that opens $env:SB_MSI so nobody can change it while the handle is open, checks its size ($env:SB_SIZE),
277
+ * SHA-256 ($env:SB_SHA256) and Authenticode signature (valid, Avouro LLC), and runs `whenGood` only if all hold. */
278
+ function recheck(whenGood) {
279
+ return [
280
+ "$ok = $false",
281
+ "$f = $null",
282
+ "try {",
283
+ " $f = [System.IO.File]::Open($env:SB_MSI, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)",
284
+ " if ($f.Length -eq [int64]$env:SB_SIZE) {",
285
+ " $sha = [System.Security.Cryptography.SHA256]::Create()",
286
+ " $h = -join ($sha.ComputeHash($f) | ForEach-Object { $_.ToString('x2') })",
287
+ " if ($h -eq $env:SB_SHA256) {",
288
+ " $s = Get-AuthenticodeSignature -LiteralPath $env:SB_MSI",
289
+ " $ok = (\"$($s.Status)\" -eq 'Valid') -and (\"$($s.SignerCertificate.Subject)\" -match '(^|,\\s*)O=Avouro LLC(,|$)')",
290
+ " }",
291
+ " }",
292
+ ...whenGood.map((line) => ` if ($ok) { ${line} }`),
293
+ "} catch { $ok = $false } finally { if ($f) { $f.Dispose() } }",
294
+ ];
295
+ }
296
+ /** The helper's check alone: exit 0 when $env:SB_MSI is still the verified installer, 3 when it is not. */
297
+ export function installerRecheckScript() {
298
+ return [...recheck([]), "if ($ok) { exit 0 } else { exit 3 }"].join("\n");
299
+ }
300
+ /** The helper script that installs after this agent exits (INSTALL_HELPER_SCRIPT). */
301
+ export function installHelperScript() {
302
+ return INSTALL_HELPER_SCRIPT;
303
+ }
304
+ /** What the last install helper recorded, once: the file is removed when read. Null when there is nothing. */
305
+ export function takeInstallResult(dir) {
306
+ const file = join(dir, "updates", INSTALL_RESULT);
307
+ try {
308
+ const raw = readFileSync(file, "utf8").replace(/^\uFEFF/, "");
309
+ rmSync(file, { force: true });
310
+ const r = JSON.parse(raw);
311
+ return { at: String(r.at ?? ""), version: String(r.version ?? ""), installed: r.installed === true, reason: typeof r.reason === "string" ? r.reason : null, exit_code: typeof r.exit_code === "number" ? r.exit_code : null };
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ }
120
317
  //# sourceMappingURL=native-update.js.map