@scopebond/agent 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +6 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -3
- package/dist/agent.js.map +1 -1
- package/dist/apps-entry.d.ts +46 -0
- package/dist/apps-entry.d.ts.map +1 -0
- package/dist/apps-entry.js +118 -0
- package/dist/apps-entry.js.map +1 -0
- package/dist/autostart.d.ts +11 -5
- package/dist/autostart.d.ts.map +1 -1
- package/dist/autostart.js +59 -5
- package/dist/autostart.js.map +1 -1
- package/dist/awake.d.ts +31 -0
- package/dist/awake.d.ts.map +1 -0
- package/dist/awake.js +57 -0
- package/dist/awake.js.map +1 -0
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/cli-main.js +28 -1
- package/dist/cli-main.js.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/ipc.d.ts +6 -2
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +11 -7
- package/dist/ipc.js.map +1 -1
- package/dist/native-update.d.ts +83 -7
- package/dist/native-update.d.ts.map +1 -1
- package/dist/native-update.js +230 -33
- package/dist/native-update.js.map +1 -1
- package/dist/selfcheck.d.ts.map +1 -1
- package/dist/selfcheck.js +4 -1
- package/dist/selfcheck.js.map +1 -1
- package/dist/service.d.ts +20 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +141 -44
- package/dist/service.js.map +1 -1
- package/dist/setup.d.ts +12 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +41 -12
- package/dist/setup.js.map +1 -1
- package/dist/tray-model.d.ts +2 -0
- package/dist/tray-model.d.ts.map +1 -1
- package/dist/tray-model.js +2 -0
- package/dist/tray-model.js.map +1 -1
- package/dist/update.d.ts +56 -12
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +209 -38
- package/dist/update.js.map +1 -1
- package/dist/upkeep.d.ts +16 -0
- package/dist/upkeep.d.ts.map +1 -0
- package/dist/upkeep.js +55 -0
- package/dist/upkeep.js.map +1 -0
- package/package.json +3 -3
package/dist/native-update.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
21
|
-
|
|
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
|
-
/**
|
|
49
|
-
|
|
50
|
-
export
|
|
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":"
|
|
1
|
+
{"version":3,"file":"native-update.d.ts","sourceRoot":"","sources":["../src/native-update.ts"],"names":[],"mappings":"AAgBA,+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;AAErD;wFACwF;AACxF,MAAM,WAAW,UAAU;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAMlF,mFAAmF;AACnF,wBAAgB,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,mEAAmE;AACnE,wBAAgB,WAAW,IAAI,UAAU,EAAE,CAG1C;AAmBD,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,GAAG,CAAC,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;AAEjK;mEACmE;AACnE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,EAAE,GAAG,SAAa,GAAG,eAAe,GAAG,IAAI,CAiBxI;AAED,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,WAAyC,CAAC;AAEvF;;mHAEmH;AACnH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,UAAU,CAIrH;AAED,kGAAkG;AAClG,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAI1E;AAED,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,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5G,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,UAAU,EAAE,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;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CAC7K,GAAG,OAAO,CAAC,iBAAiB,GAAG,OAAO,CAAC,CA8BvC;AAED,0FAA0F;AAC1F,eAAO,MAAM,cAAc,wBAAwB,CAAC;AAEpD,MAAM,WAAW,aAAa;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAEnI;;;;;;;iGAOiG;AACjG,eAAO,MAAM,qBAAqB,QA0CtB,CAAC;AAEb;6FAC6F;AAC7F,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED;4FAC4F;AAC5F,wBAAgB,cAAc,CAAC,QAAQ,SAAmB,EAAE,MAAM,UAAuB,EAAE,MAAM,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAoB,GAAG,MAAM,GAAG,IAAI,CAIxJ;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,SAAS,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAiBxR;AAED;kIACkI;AAClI,wBAAgB,gBAAgB,CAAC,SAAS,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI,CAKnL;AAED,8HAA8H;AAC9H,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC,EAAE,KAAK,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAiB,GAAG,OAAO,CAOvL;AAuBD,2GAA2G;AAC3G,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAQnE"}
|
package/dist/native-update.js
CHANGED
|
@@ -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
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
33
|
-
|
|
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
|
|
36
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
63
|
-
if (!
|
|
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"),
|
|
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
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
@@ -1 +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"]}
|
|
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 an updater key (a separate Ed25519 key; the public halves it trusts are built into this program, each with a key id\n// 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\n// the installer's Authenticode signature is valid and names Avouro LLC. The helper that runs msiexec after this agent has\n// exited checks the digest, size and signature again from a handle that keeps the file from being changed, and only then\n// starts msiexec. Anything else and nothing is installed: the agent says the update could not be verified. An install for\n// every user (Program Files) never 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 { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, win32 } 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/** One trusted updater key: its id (the first 16 hex digits of the SHA-256 of its SPKI bytes), its public half (base64\n * SPKI), and the last day it may sign (an ISO date, inclusive), or null for no end. */\nexport interface UpdaterKey { kid: string; key: string; not_after: string | null }\n\n/** Set by the single executable's build from updater-keys.json (or updater-public-key.txt): a JSON list of UpdaterKey.\n * Absent from npm builds. */\ndeclare const __SCOPEBOND_UPDATER_KEYS__: string | undefined;\n\n/** A public key's id: the first 16 hex digits of the SHA-256 of its SPKI bytes. */\nexport function keyId(publicKeyB64: string): string {\n return createHash(\"sha256\").update(Buffer.from(publicKeyB64, \"base64\")).digest(\"hex\").slice(0, 16);\n}\n\n/** The updater keys this build trusts (empty when it has none). */\nexport function updaterKeys(): UpdaterKey[] {\n if (typeof __SCOPEBOND_UPDATER_KEYS__ !== \"string\" || !__SCOPEBOND_UPDATER_KEYS__) return [];\n try { return normalizeKeys(JSON.parse(__SCOPEBOND_UPDATER_KEYS__) as unknown); } catch { return []; }\n}\n\nfunction normalizeKeys(value: unknown): UpdaterKey[] {\n if (typeof value === \"string\") return value ? [{ kid: keyId(value), key: value, not_after: null }] : [];\n if (!Array.isArray(value)) return [];\n return value.flatMap((k): UpdaterKey[] => {\n const e = k as Partial<UpdaterKey>;\n if (!e || typeof e.key !== \"string\" || !e.key) return [];\n 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 }];\n });\n}\n\n/** 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. */\nfunction inDate(key: UpdaterKey, now: number): boolean {\n if (key.not_after === null) return true;\n 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);\n return Number.isFinite(end) && now <= end;\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; kid?: string; files: Array<{ name: string; sha256: string; size: number }> }\n\n/** The manifest when its signature is one trusted, in-date updater key's over exactly these bytes, else null. A manifest\n * that names its key id (`kid`) is checked with that key only. */\nexport function verifyManifest(text: string, signatureB64: string, keys: string | UpdaterKey[], now = Date.now()): ReleaseManifest | null {\n try {\n const ring = normalizeKeys(keys).filter((k) => inDate(k, now));\n if (!ring.length) return null;\n const parsed = JSON.parse(text) as ReleaseManifest;\n const named = typeof parsed?.kid === \"string\" ? parsed.kid : null;\n const candidates = named === null ? ring : ring.filter((k) => k.kid === named);\n const message = Buffer.from(MANIFEST_DOMAIN + text, \"utf8\");\n const signature = Buffer.from(signatureB64.trim(), \"base64\");\n const signed = candidates.some((k) => {\n try { return verify(null, message, createPublicKey({ key: Buffer.from(k.key, \"base64\"), format: \"der\", type: \"spki\" }), signature); }\n catch { return false; }\n });\n if (!signed) return null;\n if (parsed.type !== \"scopebond:agent-release\" || typeof parsed.version !== \"string\" || !Array.isArray(parsed.files)) return null;\n return parsed;\n } catch { return null; }\n}\n\nexport const installerName = (version: string) => `scopebond-agent-${version}-x64.msi`;\n\n/** The environment for Windows PowerShell (powershell.exe, 5.1): this one's, without `PSModulePath`, plus `extra`. An\n * agent started from PowerShell 7 inherits its module path, and Windows PowerShell then cannot load its own security\n * module (Get-AuthenticodeSignature), so every check would fail; without the variable it uses its own default. */\nexport function powershellEnv(extra: Record<string, string>, base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n for (const [key, value] of Object.entries(base)) if (key.toLowerCase() !== \"psmodulepath\") env[key] = value;\n return { ...env, ...extra };\n}\n\n/** A Windows tool by its full path under the system folder, never by a name looked up on PATH. */\nexport function windowsTool(name: \"powershell\" | \"msiexec\" | \"cmd\"): string {\n const root = process.env.SystemRoot || process.env.windir || \"C:\\\\Windows\";\n if (name === \"powershell\") return win32.join(root, \"System32\", \"WindowsPowerShell\", \"v1.0\", \"powershell.exe\");\n return win32.join(root, \"System32\", `${name}.exe`);\n}\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(windowsTool(\"powershell\"), [\"-NoProfile\", \"-NonInteractive\", \"-Command\", script], { encoding: \"utf8\", env: powershellEnv({ 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; sha256: string; size: number }\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 | UpdaterKey[] | null; check?: (file: string) => { valid: boolean; subject: string }; base?: string; now?: number;\n}): Promise<VerifiedInstaller | Refused> {\n const keys = o.publicKey === undefined ? updaterKeys() : o.publicKey === null ? [] : normalizeKeys(o.publicKey);\n if (!keys.length) 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\"), keys, o.now);\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, sha256: entry.sha256, size: entry.size };\n}\n\n/** What the helper that installs after this agent exits records, beside the installer. */\nexport const INSTALL_RESULT = \"install-result.json\";\n\nexport interface InstallResult { at: string; version: string; installed: boolean; reason: string | null; exit_code: number | null }\n\n/** The helper's script. It is constant text: every value (the installer, its digest and size, the publisher rule, where to\n * record the outcome) reaches it through environment variables, never through the script itself.\n *\n * Between the download and the install the agent exits and up to a minute passes, so the helper checks the installer again\n * right before it runs msiexec, the same way the agent did: its size and SHA-256 are the signed manifest's, its Authenticode\n * signature is valid and its signer matches the same anchored publisher rule (PUBLISHER). It holds the file open, denying\n * writes and deletes, from that check until msiexec has finished, so the file checked is the file installed. Any mismatch\n * installs nothing and records why in INSTALL_RESULT; the agent is started again either way. */\nexport const INSTALL_HELPER_SCRIPT = [\n \"$ErrorActionPreference = 'Stop'\",\n \"function Write-Result([bool] $installed, $reason, $code) {\",\n \" try {\",\n \" $json = [ordered]@{ at = [DateTime]::UtcNow.ToString('o'); version = $env:SB_VERSION; installed = $installed; reason = $reason; exit_code = $code } | ConvertTo-Json -Compress\",\n \" [IO.File]::WriteAllText($env:SB_RESULT, $json, (New-Object Text.UTF8Encoding $false))\",\n \" } catch { }\",\n \"}\",\n // The native tray first: it would start the old agent again, and the installer replaces its file. Only this install's.\n \"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 { } }\",\n \"try { Wait-Process -Id ([int]$env:SB_PID) -Timeout 60 -ErrorAction SilentlyContinue } catch { }\",\n \"$lock = $null\",\n \"$reason = $null\",\n \"try {\",\n \" $lock = New-Object IO.FileStream($env:SB_MSI, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)\",\n \" if ($lock.Length -ne [long]$env:SB_SIZE) { $reason = \\\"the installer is $($lock.Length) bytes, not the manifest's $($env:SB_SIZE)\\\" }\",\n \" else {\",\n \" $sha = [Security.Cryptography.SHA256]::Create()\",\n \" $digest = ([BitConverter]::ToString($sha.ComputeHash($lock)) -replace '-', '').ToLowerInvariant()\",\n \" if ($digest -cne $env:SB_SHA256.ToLowerInvariant()) { $reason = 'the installer changed after it was checked: its SHA-256 is not the manifest''s' }\",\n \" else {\",\n \" $sig = Get-AuthenticodeSignature -LiteralPath $env:SB_MSI\",\n \" $subject = \\\"$($sig.SignerCertificate.Subject)\\\"\",\n \" if (\\\"$($sig.Status)\\\" -ne 'Valid') { $reason = \\\"the installer's signature is not valid ($($sig.Status))\\\" }\",\n \" elseif (-not ($subject -cmatch $env:SB_PUBLISHER)) { $reason = \\\"the installer is signed by $subject, not the expected publisher\\\" }\",\n \" }\",\n \" }\",\n \"} catch { $reason = \\\"the installer could not be checked: $($_.Exception.Message)\\\" }\",\n \"try {\",\n \" if ($reason) { Write-Result $false $reason $null }\",\n \" else {\",\n \" try {\",\n // Windows' own msiexec by its full path (SB_MSIEXEC only replaces it in tests).\n \" $msiexec = if ($env:SB_MSIEXEC) { $env:SB_MSIEXEC } else { Join-Path $env:SystemRoot 'System32\\\\msiexec.exe' }\",\n \" $p = Start-Process -FilePath $msiexec -ArgumentList @('/i', ('\\\"' + $env:SB_MSI + '\\\"'), '/qn', '/norestart') -Wait -PassThru\",\n \" $code = [int]$p.ExitCode\",\n \" if ($code -eq 0 -or $code -eq 3010) { Write-Result $true $null $code } else { Write-Result $false \\\"msiexec ended with exit code $code\\\" $code }\",\n \" } catch { Write-Result $false \\\"msiexec could not start: $($_.Exception.Message)\\\" $null }\",\n \" }\",\n \"} finally { if ($lock) { $lock.Dispose() } }\",\n // Installed or not, the tray starts again (it starts the agent), or else the agent through autostart's launcher.\n \"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 }\",\n].join(\"\\n\");\n\n/** The helper's script (INSTALL_HELPER_SCRIPT): stop the native tray, wait for this agent to exit, check the installer\n * again, install, then start the tray again (or the agent through autostart's launcher). */\nexport function installAfterExitScript(): string {\n return INSTALL_HELPER_SCRIPT;\n}\n\n/** The native Scopebond tray the installer puts beside the single executable (`scopebond-tray.exe`), when it is there. It\n * starts the agent at sign-in and keeps it running, so the agent leaves the tray to it. */\nexport function nativeTrayPath(execPath = process.execPath, single = isSingleExecutable(), exists: (path: string) => boolean = existsSync): string | null {\n if (!single) return null;\n const tray = win32.join(win32.dirname(execPath), \"scopebond-tray.exe\");\n return exists(tray) ? tray : null;\n}\n\n/** The helper's program, arguments and environment values (pure, so it can be tested). */\nexport function installHelper(installer: { path: string; version: string; sha256: string; size: number }, pid: number, launcher: string | null, o: { msiexec?: string; publisher?: RegExp; tray?: string | null } = {}): { program: string; args: string[]; env: Record<string, string> } {\n return {\n program: windowsTool(\"powershell\"),\n args: [\"-NoProfile\", \"-NonInteractive\", \"-WindowStyle\", \"Hidden\", \"-Command\", INSTALL_HELPER_SCRIPT],\n env: {\n SB_PID: String(pid),\n SB_MSI: installer.path,\n SB_VERSION: installer.version,\n SB_SHA256: installer.sha256,\n SB_SIZE: String(installer.size),\n SB_PUBLISHER: (o.publisher ?? PUBLISHER).source,\n SB_RESULT: join(dirname(installer.path), INSTALL_RESULT),\n SB_MSIEXEC: o.msiexec ?? windowsTool(\"msiexec\"),\n SB_LAUNCHER: launcher ?? \"\",\n SB_TRAY: o.tray ?? \"\",\n },\n };\n}\n\n/** Install it once this agent has exited, then start the tray again (or the agent through autostart's launcher). A detached\n * helper does it, because the installer replaces this very file; it checks the installer again first (INSTALL_HELPER_SCRIPT). */\nexport function installAfterExit(installer: { path: string; version: string; sha256: string; size: number }, pid: number, launcher: string | null, tray: string | null = null): void {\n const helper = installHelper(installer, pid, launcher, { tray });\n const child = spawn(helper.program, helper.args, { detached: true, stdio: \"ignore\", windowsHide: true, env: powershellEnv(helper.env) });\n child.on(\"error\", () => { /* the next sign-in starts the agent */ });\n child.unref();\n}\n\n/** Whether the installer on disk is still the verified one: same size and SHA-256, and still validly signed by Avouro LLC. */\nexport function installerUnchanged(installer: Pick<VerifiedInstaller, \"path\" | \"sha256\" | \"size\">, check: (file: string) => { valid: boolean; subject: string } = authenticode): boolean {\n try {\n const bytes = readFileSync(installer.path);\n if (bytes.length !== installer.size || createHash(\"sha256\").update(bytes).digest(\"hex\") !== installer.sha256) return false;\n const signed = check(installer.path);\n return signed.valid && PUBLISHER.test(signed.subject);\n } catch { return false; }\n}\n\n/** PowerShell that opens $env:SB_MSI so nobody can change it while the handle is open, checks its size ($env:SB_SIZE),\n * SHA-256 ($env:SB_SHA256) and Authenticode signature (valid, Avouro LLC), and runs `whenGood` only if all hold. */\nfunction recheck(whenGood: string[]): string[] {\n return [\n \"$ok = $false\",\n \"$f = $null\",\n \"try {\",\n \" $f = [System.IO.File]::Open($env:SB_MSI, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)\",\n \" if ($f.Length -eq [int64]$env:SB_SIZE) {\",\n \" $sha = [System.Security.Cryptography.SHA256]::Create()\",\n \" $h = -join ($sha.ComputeHash($f) | ForEach-Object { $_.ToString('x2') })\",\n \" if ($h -eq $env:SB_SHA256) {\",\n \" $s = Get-AuthenticodeSignature -LiteralPath $env:SB_MSI\",\n \" $ok = (\\\"$($s.Status)\\\" -eq 'Valid') -and (\\\"$($s.SignerCertificate.Subject)\\\" -match '(^|,\\\\s*)O=Avouro LLC(,|$)')\",\n \" }\",\n \" }\",\n ...whenGood.map((line) => ` if ($ok) { ${line} }`),\n \"} catch { $ok = $false } finally { if ($f) { $f.Dispose() } }\",\n ];\n}\n\n/** The helper's check alone: exit 0 when $env:SB_MSI is still the verified installer, 3 when it is not. */\nexport function installerRecheckScript(): string {\n return [...recheck([]), \"if ($ok) { exit 0 } else { exit 3 }\"].join(\"\\n\");\n}\n\n/** The helper script that installs after this agent exits (INSTALL_HELPER_SCRIPT). */\nexport function installHelperScript(): string {\n return INSTALL_HELPER_SCRIPT;\n}\n\n/** What the last install helper recorded, once: the file is removed when read. Null when there is nothing. */\nexport function takeInstallResult(dir: string): InstallResult | null {\n const file = join(dir, \"updates\", INSTALL_RESULT);\n try {\n const raw = readFileSync(file, \"utf8\").replace(/^\\uFEFF/, \"\");\n rmSync(file, { force: true });\n const r = JSON.parse(raw) as Partial<InstallResult>;\n 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 };\n } catch { return null; }\n}\n"]}
|
package/dist/selfcheck.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selfcheck.d.ts","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,OAAO,EAAE,KAAK,cAAc,EAClC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"selfcheck.d.ts","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,OAAO,EAAE,KAAK,cAAc,EAClC,MAAM,iBAAiB,CAAC;AAIzB,MAAM,WAAW,aAAa;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AAE3E,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,GAAG,SAAa,GAAG,aAAa,EAAE,CAmB5H;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EACnF,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3E,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC,CAyBzG"}
|
package/dist/selfcheck.js
CHANGED
|
@@ -9,6 +9,7 @@ import { canonical } from "@scopebond/gateway";
|
|
|
9
9
|
import { loadOrCreateAttester } from "@scopebond/gateway/node";
|
|
10
10
|
import { configuredHookCommands, duplicateHooks, hookCommandResolves, hookVersion, isScopebondHookCommand, queueStatus, readDeliveryState, userHarnessFile, } from "@scopebond/hook";
|
|
11
11
|
import { autostartHealth } from "./autostart.js";
|
|
12
|
+
import { installKind } from "./native-update.js";
|
|
12
13
|
export function selfCheckProof(credentialId, gatewayId, day) {
|
|
13
14
|
return canonical({ type: "scopebond:self-check", version: 1, credential_id: credentialId, gateway_id: gatewayId, day });
|
|
14
15
|
}
|
|
@@ -45,7 +46,9 @@ export async function runSelfCheck(dir, connection, harnesses, agentVersion, opt
|
|
|
45
46
|
const res = await (options.fetchImpl ?? fetch)(new URL("/v1/self-check", connection.url).toString(), {
|
|
46
47
|
method: "POST", redirect: "error", signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),
|
|
47
48
|
headers: { authorization: `Bearer ${connection.credential}`, "content-type": "application/json" },
|
|
48
|
-
|
|
49
|
+
// install_kind: how this agent was installed (npm, per-user or per-machine signed installer), so the workspace can say
|
|
50
|
+
// how the computer updates and is removed. A workspace that does not read it ignores it.
|
|
51
|
+
body: JSON.stringify({ signature, day, checks, hook_version: hookVersion(), agent_version: agentVersion, install_kind: installKind() }),
|
|
49
52
|
});
|
|
50
53
|
// A workspace that does not offer the self-check yet answers 404: the computer's own checks still count; the signed
|
|
51
54
|
// workspace part is simply not available there.
|