vault-go 0.11.0 → 0.13.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.
@@ -0,0 +1,185 @@
1
+ import { randomUUID, createHash } from "node:crypto";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { hostname, platform } from "node:os";
5
+ import { loadConfig, loadTokens, saveTokens } from "./config.js";
6
+ import { VaultCloudError } from "./cloud.js";
7
+ export function localDevice(home, cloud, storage, version) {
8
+ const owner = () => createHash("sha256")
9
+ .update(loadConfig(home).apiUrl + "|" + (loadConfig(home).email ?? ""))
10
+ .digest("hex");
11
+ let status = { status: "unregistered" };
12
+ let inFlight = null;
13
+ let stopped = false;
14
+ const record = () => {
15
+ const d = storage?.read("local-device.json");
16
+ return d?.owner === owner() ? d : null;
17
+ };
18
+ function adopt(value, requestOwner) {
19
+ const made = value;
20
+ if (!/^[a-f0-9-]{36}$/.test(made.id) ||
21
+ !/^vg_device_[a-f0-9]{64}$/.test(made.deviceToken) ||
22
+ typeof made.accessToken !== "string" ||
23
+ typeof made.refreshToken !== "string")
24
+ throw new Error("Invalid registration response.");
25
+ const device = {
26
+ id: made.id,
27
+ token: made.deviceToken,
28
+ owner: requestOwner,
29
+ };
30
+ storage.write("local-device.json", device);
31
+ saveTokens({
32
+ accessToken: made.accessToken,
33
+ refreshToken: made.refreshToken,
34
+ expiresIn: made.expiresIn,
35
+ expiresAt: Date.now() + (made.expiresIn || 900) * 1000,
36
+ }, home);
37
+ return device;
38
+ }
39
+ function installationId() {
40
+ const path = join(home, "local-installation.json");
41
+ try {
42
+ const value = JSON.parse(readFileSync(path, "utf8"));
43
+ if (/^[a-f0-9-]{36}$/.test(value.id))
44
+ return value.id;
45
+ throw new Error("Invalid installation identity.");
46
+ }
47
+ catch (error) {
48
+ if (error.code !== "ENOENT")
49
+ throw error;
50
+ const id = randomUUID();
51
+ writeFileSync(path, JSON.stringify({ id }), { mode: 0o600, flag: "wx" });
52
+ return id;
53
+ }
54
+ }
55
+ async function heartbeat() {
56
+ if (stopped)
57
+ return;
58
+ if (inFlight)
59
+ return inFlight;
60
+ if (!loadTokens(home) || !storage) {
61
+ status = {
62
+ status: "unregistered",
63
+ ...(!storage ? { error: "cache_key_unavailable" } : {}),
64
+ };
65
+ return;
66
+ }
67
+ const requestOwner = owner();
68
+ inFlight = Promise.resolve().then(async () => {
69
+ try {
70
+ let device = record();
71
+ if (device?.revoked) {
72
+ status = { status: "revoked" };
73
+ return;
74
+ }
75
+ if (device) {
76
+ let binding;
77
+ try {
78
+ binding = JSON.parse(Buffer.from(loadTokens(home).accessToken.split(".")[1], "base64url").toString()).deviceId;
79
+ }
80
+ catch { }
81
+ if (binding !== device.id) {
82
+ const before = loadTokens(home)?.accessToken;
83
+ const made = await cloud.rotateDevice(device.id);
84
+ if (stopped ||
85
+ requestOwner !== owner() ||
86
+ before !== loadTokens(home)?.accessToken)
87
+ return;
88
+ device = adopt(made, requestOwner);
89
+ }
90
+ }
91
+ if (!device) {
92
+ const before = loadTokens(home);
93
+ const made = await cloud.registerDevice({
94
+ name: hostname().slice(0, 80),
95
+ platform: platform(),
96
+ installationId: installationId(),
97
+ agentVersion: version,
98
+ localProtection: "encrypted-cache",
99
+ });
100
+ if (stopped ||
101
+ requestOwner !== owner() ||
102
+ loadTokens(home)?.accessToken !== before?.accessToken)
103
+ return;
104
+ device = adopt(made, requestOwner);
105
+ }
106
+ const result = (await cloud.deviceAction(device.id, device.token, "heartbeat", { agentVersion: version, localProtection: "encrypted-cache" }));
107
+ if (stopped || requestOwner !== owner())
108
+ return;
109
+ status = {
110
+ status: "online",
111
+ ...(result.name ? { name: result.name } : {}),
112
+ ...(result.lastSeenAt ? { lastSeenAt: result.lastSeenAt } : {}),
113
+ };
114
+ }
115
+ catch (error) {
116
+ if (stopped || requestOwner !== owner())
117
+ return;
118
+ if (error instanceof VaultCloudError &&
119
+ (error.status === 401 || error.status === 409)) {
120
+ const current = record();
121
+ if (current)
122
+ storage.write("local-device.json", { ...current, revoked: true });
123
+ status = {
124
+ status: "revoked",
125
+ error: "device_reauthorization_required",
126
+ };
127
+ }
128
+ else
129
+ status = {
130
+ status: "offline",
131
+ error: error instanceof VaultCloudError && error.status === 403
132
+ ? "browser_reauthentication_required"
133
+ : "device_connection_unavailable",
134
+ };
135
+ }
136
+ finally {
137
+ inFlight = null;
138
+ }
139
+ });
140
+ return inFlight;
141
+ }
142
+ return {
143
+ heartbeat,
144
+ close() {
145
+ stopped = true;
146
+ },
147
+ status: () => ({ ...status }),
148
+ async disconnect() {
149
+ await inFlight;
150
+ const current = record();
151
+ if (current && !current.revoked) {
152
+ try {
153
+ await cloud.deviceAction(current.id, current.token, "disconnect");
154
+ }
155
+ catch (error) {
156
+ if (!(error instanceof VaultCloudError && error.status === 401))
157
+ throw error;
158
+ }
159
+ }
160
+ if (current)
161
+ storage?.write("local-device.json", { ...current, revoked: true });
162
+ status = { status: "revoked" };
163
+ },
164
+ // New browser authentication is explicit user consent to authorize this installation again.
165
+ async reauthorize() {
166
+ if (!storage)
167
+ throw new Error("Encrypted device credential storage is unavailable.");
168
+ await inFlight;
169
+ const current = record();
170
+ const requestOwner = owner(), before = loadTokens(home)?.accessToken;
171
+ if (current) {
172
+ const made = await cloud.rotateDevice(current.id);
173
+ if (stopped ||
174
+ requestOwner !== owner() ||
175
+ before !== loadTokens(home)?.accessToken)
176
+ return;
177
+ adopt(made, requestOwner);
178
+ }
179
+ else if (status.status === "revoked") {
180
+ writeFileSync(join(home, "local-installation.json"), JSON.stringify({ id: randomUUID() }), { mode: 0o600 });
181
+ }
182
+ await heartbeat();
183
+ },
184
+ };
185
+ }
@@ -1,7 +1,7 @@
1
1
  export declare const LOCAL_PORT = 38850;
2
2
  export declare const localUrl = "http://localhost:38850/";
3
3
  export interface LocalHealth {
4
- service: 'vault-go';
4
+ service: "vault-go";
5
5
  version: string;
6
6
  port: number;
7
7
  }
@@ -11,7 +11,14 @@ export declare function startLocal(home?: string): Promise<LocalHealth>;
11
11
  export declare function openLocal(home?: string): Promise<void>;
12
12
  export declare function stopLocal(home?: string): Promise<boolean>;
13
13
  export declare function launchAgentXml(executable: string, worker: string, home: string): string;
14
+ interface LaunchAgentOperations {
15
+ run: (args: string[]) => Promise<void>;
16
+ wait: (milliseconds: number) => Promise<void>;
17
+ report: (message: string) => void;
18
+ }
19
+ export declare function replaceLaunchAgent(domain: string, path: string, prepare: () => Promise<void>, operations: LaunchAgentOperations): Promise<void>;
14
20
  export declare function installLocal(home?: string): Promise<{
15
21
  startup: boolean;
16
22
  }>;
17
23
  export declare function uninstallLocal(home?: string): Promise<void>;
24
+ export {};
@@ -1,89 +1,120 @@
1
- import { execFile, spawn } from 'node:child_process';
2
- import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
3
- import { chmodSync, copyFileSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { dirname, join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { promisify } from 'node:util';
8
- import { vaultHome } from './config.js';
9
- import { openVaultBrowser } from './auth.js';
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import { vaultHome } from "./config.js";
9
+ import { openVaultBrowser } from "./auth.js";
10
10
  const execute = promisify(execFile);
11
11
  export const LOCAL_PORT = 38850;
12
- const LABEL = 'com.resolveup.vault-go';
13
- const OWNER = '<!-- Managed by vault-go local install -->';
12
+ const LABEL = "com.resolveup.vault-go";
13
+ const OWNER = "<!-- Managed by vault-go local install -->";
14
14
  export const localUrl = `http://localhost:${LOCAL_PORT}/`;
15
15
  export async function localStatus(port = LOCAL_PORT, home = vaultHome()) {
16
- const challenge = randomBytes(32).toString('base64url');
16
+ const challenge = randomBytes(32).toString("base64url");
17
17
  let response;
18
18
  try {
19
- response = await fetch(`http://127.0.0.1:${port}/health?challenge=${challenge}`, { signal: AbortSignal.timeout(1500), redirect: 'error' });
19
+ response = await fetch(`http://127.0.0.1:${port}/health?challenge=${challenge}`, { signal: AbortSignal.timeout(1500), redirect: "error" });
20
20
  }
21
21
  catch {
22
22
  return null;
23
23
  }
24
24
  let body;
25
25
  try {
26
- body = await response.json();
26
+ body = (await response.json());
27
27
  }
28
28
  catch {
29
29
  throw new Error(`Port ${port} is occupied by another service.`);
30
30
  }
31
- if (!response.ok || body.service !== 'vault-go' || typeof body.version !== 'string' || !/^\d+\.\d+\.\d+/.test(body.version) || body.port !== port) {
31
+ if (!response.ok ||
32
+ body.service !== "vault-go" ||
33
+ typeof body.version !== "string" ||
34
+ !/^\d+\.\d+\.\d+/.test(body.version) ||
35
+ body.port !== port) {
32
36
  throw new Error(`Port ${port} is occupied by another service.`);
33
37
  }
34
- const expected = createHmac('sha256', localToken(home)).update(challenge).digest();
35
- const received = Buffer.from(body.proof ?? '', 'base64url');
36
- if (expected.length !== received.length || !timingSafeEqual(expected, received))
38
+ const expected = createHmac("sha256", localToken(home))
39
+ .update(challenge)
40
+ .digest();
41
+ const received = Buffer.from(body.proof ?? "", "base64url");
42
+ if (expected.length !== received.length ||
43
+ !timingSafeEqual(expected, received))
37
44
  throw new Error(`Could not verify the Vault service on port ${port}.`);
38
45
  return { service: body.service, version: body.version, port: body.port };
39
46
  }
40
47
  export function stageLocalRuntime(home = vaultHome(), source = dirname(fileURLToPath(import.meta.url))) {
41
- const manifest = JSON.parse(readFileSync(join(source, '..', 'package.json'), 'utf8'));
42
- const root = join(home, 'local-runtime');
43
- const target = join(root, 'dist');
48
+ const manifest = JSON.parse(readFileSync(join(source, "..", "package.json"), "utf8"));
49
+ const root = join(home, "local-runtime");
50
+ const target = join(root, "dist");
44
51
  mkdirSync(target, { recursive: true, mode: 0o700 });
45
52
  chmodSync(root, 0o700);
46
53
  chmodSync(target, 0o700);
47
- const excluded = new Set(['index.js', 'server.js', 'installer.js', 'cli.js', 'auth.js']);
48
- if (!existsSync(join(source, 'local-worker.js')))
49
- throw new Error('Local worker is missing. Build or reinstall vault-go first.');
54
+ const excluded = new Set(["index.js", "server.js", "installer.js", "cli.js"]);
55
+ if (!existsSync(join(source, "local-worker.js")))
56
+ throw new Error("Local worker is missing. Build or reinstall vault-go first.");
50
57
  for (const file of readdirSync(source)) {
51
- if (!file.endsWith('.js') || file.endsWith('.test.js') || excluded.has(file))
58
+ if (!file.endsWith(".js") ||
59
+ file.endsWith(".test.js") ||
60
+ excluded.has(file))
52
61
  continue;
53
62
  const destination = join(target, file);
54
63
  copyFileSync(join(source, file), `${destination}.tmp`);
55
64
  chmodSync(`${destination}.tmp`, 0o600);
56
65
  renameSync(`${destination}.tmp`, destination);
57
66
  }
58
- writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'vault-go-local-runtime', version: manifest.version, type: 'module' }), { mode: 0o600 });
59
- return join(target, 'local-worker.js');
67
+ const assets = join(root, "assets");
68
+ mkdirSync(assets, { recursive: true, mode: 0o700 });
69
+ const logo = join(source, "..", "assets", "vault-logo-glass.png");
70
+ if (existsSync(logo)) {
71
+ copyFileSync(logo, join(assets, "vault-logo-glass.png"));
72
+ chmodSync(join(assets, "vault-logo-glass.png"), 0o600);
73
+ }
74
+ writeFileSync(join(root, "package.json"), JSON.stringify({
75
+ name: "vault-go-local-runtime",
76
+ version: manifest.version,
77
+ type: "module",
78
+ }), { mode: 0o600 });
79
+ return join(target, "local-worker.js");
60
80
  }
61
81
  export async function startLocal(home = vaultHome()) {
62
82
  const running = await localStatus(LOCAL_PORT, home);
63
83
  if (running)
64
84
  return running;
65
85
  const worker = stageLocalRuntime(home);
66
- const log = openSync(join(home, 'local-service.log'), 'a', 0o600);
86
+ const log = openSync(join(home, "local-service.log"), "a", 0o600);
67
87
  try {
68
- const child = spawn(process.execPath, [worker], { detached: true, stdio: ['ignore', log, log], env: { ...process.env, VAULT_GO_HOME: home, VAULT_GO_LOCAL_PORT: String(LOCAL_PORT) } });
69
- await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
88
+ const child = spawn(process.execPath, [worker], {
89
+ detached: true,
90
+ stdio: ["ignore", log, log],
91
+ env: {
92
+ ...process.env,
93
+ VAULT_GO_HOME: home,
94
+ VAULT_GO_LOCAL_PORT: String(LOCAL_PORT),
95
+ },
96
+ });
97
+ await new Promise((resolve, reject) => {
98
+ child.once("spawn", resolve);
99
+ child.once("error", reject);
100
+ });
70
101
  child.unref();
71
102
  }
72
103
  finally {
73
104
  closeSync(log);
74
105
  }
75
106
  for (let attempt = 0; attempt < 40; attempt++) {
76
- await new Promise(resolve => setTimeout(resolve, 150));
107
+ await new Promise((resolve) => setTimeout(resolve, 150));
77
108
  const status = await localStatus(LOCAL_PORT, home);
78
109
  if (status)
79
110
  return status;
80
111
  }
81
- throw new Error(`Local service did not start. Check ${join(home, 'local-service.log')}.`);
112
+ throw new Error(`Local service did not start. Check ${join(home, "local-service.log")}.`);
82
113
  }
83
114
  function localToken(home) {
84
- const value = JSON.parse(readFileSync(join(home, 'local-access.json'), 'utf8'));
115
+ const value = JSON.parse(readFileSync(join(home, "local-access.json"), "utf8"));
85
116
  if (!value.token || !/^[A-Za-z0-9_-]{32,}$/.test(value.token))
86
- throw new Error('Local access token is missing or invalid.');
117
+ throw new Error("Local access token is missing or invalid.");
87
118
  return value.token;
88
119
  }
89
120
  export async function openLocal(home = vaultHome()) {
@@ -92,32 +123,102 @@ export async function openLocal(home = vaultHome()) {
92
123
  await openVaultBrowser(`${localUrl}#token=${encodeURIComponent(localToken(home))}`);
93
124
  }
94
125
  catch {
95
- throw new Error('Could not open the local dashboard. Retry: vault-go local open');
126
+ throw new Error("Could not open the local dashboard. Retry: vault-go local open");
96
127
  }
97
128
  }
98
129
  export async function stopLocal(home = vaultHome()) {
99
- if (!await localStatus(LOCAL_PORT, home))
130
+ if (!(await localStatus(LOCAL_PORT, home)))
100
131
  return false;
101
- const result = await fetch(`http://127.0.0.1:${LOCAL_PORT}/api/shutdown`, { method: 'POST', headers: { Authorization: `Bearer ${localToken(home)}` }, signal: AbortSignal.timeout(3000), redirect: 'error' });
132
+ const result = await fetch(`http://127.0.0.1:${LOCAL_PORT}/api/shutdown`, {
133
+ method: "POST",
134
+ headers: { Authorization: `Bearer ${localToken(home)}` },
135
+ signal: AbortSignal.timeout(3000),
136
+ redirect: "error",
137
+ });
102
138
  if (!result.ok)
103
139
  throw new Error(`Local shutdown refused (${result.status}).`);
104
140
  for (let attempt = 0; attempt < 20; attempt++) {
105
- if (!await localStatus(LOCAL_PORT, home))
141
+ if (!(await localStatus(LOCAL_PORT, home)))
106
142
  return true;
107
- await new Promise(resolve => setTimeout(resolve, 100));
143
+ await new Promise((resolve) => setTimeout(resolve, 100));
108
144
  }
109
- throw new Error('Local service has not stopped yet.');
145
+ throw new Error("Local service has not stopped yet.");
110
146
  }
111
147
  export function launchAgentXml(executable, worker, home) {
112
- const escape = (value) => value.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' }[c]));
113
- return `<?xml version="1.0" encoding="UTF-8"?>\n${OWNER}\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${LABEL}</string><key>ProgramArguments</key><array><string>${escape(executable)}</string><string>${escape(worker)}</string></array><key>EnvironmentVariables</key><dict><key>VAULT_GO_HOME</key><string>${escape(home)}</string><key>VAULT_GO_LOCAL_PORT</key><string>${LOCAL_PORT}</string></dict><key>RunAtLoad</key><true/><key>StandardOutPath</key><string>${escape(join(home, 'local-service.log'))}</string><key>StandardErrorPath</key><string>${escape(join(home, 'local-service.log'))}</string></dict></plist>\n`;
148
+ const escape = (value) => value.replace(/[&<>"']/g, (c) => ({
149
+ "&": "&amp;",
150
+ "<": "&lt;",
151
+ ">": "&gt;",
152
+ '"': "&quot;",
153
+ "'": "&apos;",
154
+ })[c]);
155
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${OWNER}\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${LABEL}</string><key>ProgramArguments</key><array><string>${escape(executable)}</string><string>${escape(worker)}</string></array><key>EnvironmentVariables</key><dict><key>VAULT_GO_HOME</key><string>${escape(home)}</string><key>VAULT_GO_LOCAL_PORT</key><string>${LOCAL_PORT}</string><key>PATH</key><string>${escape(process.env["PATH"] || "/usr/local/bin:/usr/bin:/bin")}</string></dict><key>RunAtLoad</key><true/><key>StandardOutPath</key><string>${escape(join(home, "local-service.log"))}</string><key>StandardErrorPath</key><string>${escape(join(home, "local-service.log"))}</string></dict></plist>\n`;
156
+ }
157
+ function agentPath() {
158
+ return join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
159
+ }
160
+ function assertOwned(path) {
161
+ if (existsSync(path) && !readFileSync(path, "utf8").includes(OWNER))
162
+ throw new Error(`Refusing to replace an unowned startup file: ${path}`);
163
+ }
164
+ export async function replaceLaunchAgent(domain, path, prepare, operations) {
165
+ const target = `${domain}/${LABEL}`;
166
+ const codeOf = (error) => error?.code;
167
+ const isAbsent = async () => {
168
+ try {
169
+ await operations.run(["print", target]);
170
+ return false;
171
+ }
172
+ catch (error) {
173
+ // launchctl uses 113 for an unknown service. Other failures do not prove removal.
174
+ const stderr = String(error?.stderr ?? "");
175
+ if (codeOf(error) === 113 &&
176
+ stderr.includes(`Could not find service "${LABEL}"`))
177
+ return true;
178
+ throw error;
179
+ }
180
+ };
181
+ let bootoutError;
182
+ try {
183
+ await operations.run(["bootout", target]);
184
+ }
185
+ catch (error) {
186
+ bootoutError = error;
187
+ }
188
+ for (let attempt = 0;; attempt++) {
189
+ if (await isAbsent()) {
190
+ if (bootoutError &&
191
+ codeOf(bootoutError) !== 3 &&
192
+ codeOf(bootoutError) !== 113)
193
+ throw bootoutError;
194
+ break;
195
+ }
196
+ if (bootoutError)
197
+ throw bootoutError;
198
+ if (attempt === 39)
199
+ throw new Error(`LaunchAgent ${target} is still registered after bootout; bootstrap was not attempted.`);
200
+ await operations.wait(250);
201
+ }
202
+ await prepare();
203
+ for (let attempt = 0; attempt < 3; attempt++) {
204
+ try {
205
+ await operations.run(["bootstrap", domain, path]);
206
+ return;
207
+ }
208
+ catch (error) {
209
+ // Error 5 can be a teardown race, but is also a genuine configuration failure.
210
+ // Retry only when print independently proves that no job was registered.
211
+ if (codeOf(error) !== 5 || attempt === 2 || !(await isAbsent()))
212
+ throw error;
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ operations.report(`launchctl bootstrap: ${message}\nConfirmed ${target} is absent; retrying (${attempt + 2}/3).`);
215
+ await operations.wait(500 * (attempt + 1));
216
+ }
217
+ }
114
218
  }
115
- function agentPath() { return join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`); }
116
- function assertOwned(path) { if (existsSync(path) && !readFileSync(path, 'utf8').includes(OWNER))
117
- throw new Error(`Refusing to replace an unowned startup file: ${path}`); }
118
219
  export async function installLocal(home = vaultHome()) {
119
220
  const worker = stageLocalRuntime(home);
120
- if (process.platform !== 'darwin') {
221
+ if (process.platform !== "darwin") {
121
222
  await startLocal(home);
122
223
  return { startup: false };
123
224
  }
@@ -126,34 +227,46 @@ export async function installLocal(home = vaultHome()) {
126
227
  await localStatus(LOCAL_PORT, home);
127
228
  mkdirSync(dirname(path), { recursive: true });
128
229
  const domain = `gui/${process.getuid()}`;
129
- // An absent registration is expected on the first installation.
130
- try {
131
- await execute('launchctl', ['bootout', `${domain}/${LABEL}`]);
132
- }
133
- catch { }
134
- await stopLocal(home);
135
- writeFileSync(path, launchAgentXml(process.execPath, worker, home), { mode: 0o600 });
136
- await execute('launchctl', ['bootstrap', domain, path]);
230
+ await replaceLaunchAgent(domain, path, async () => {
231
+ await stopLocal(home);
232
+ writeFileSync(path, launchAgentXml(process.execPath, worker, home), {
233
+ mode: 0o600,
234
+ });
235
+ }, {
236
+ run: async (args) => {
237
+ await execute("launchctl", args, { timeout: 5000 });
238
+ },
239
+ wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
240
+ report: (message) => {
241
+ process.stderr.write(`${message}\n`);
242
+ },
243
+ });
137
244
  for (let attempt = 0; attempt < 40; attempt++) {
138
245
  const health = await localStatus(LOCAL_PORT, home);
139
246
  if (health)
140
247
  return { startup: true };
141
- await new Promise(resolve => setTimeout(resolve, 150));
248
+ await new Promise((resolve) => setTimeout(resolve, 150));
142
249
  }
143
- throw new Error('LaunchAgent was registered, but the local service did not start. Check local-service.log.');
250
+ throw new Error("LaunchAgent was registered, but the local service did not start. Check local-service.log.");
144
251
  }
145
252
  export async function uninstallLocal(home = vaultHome()) {
146
- if (process.platform === 'darwin') {
253
+ if (process.platform === "darwin") {
147
254
  const path = agentPath();
148
255
  assertOwned(path);
149
256
  if (existsSync(path)) {
150
257
  try {
151
- await execute('launchctl', ['bootout', `gui/${process.getuid()}/${LABEL}`]);
258
+ await execute("launchctl", [
259
+ "bootout",
260
+ `gui/${process.getuid()}/${LABEL}`,
261
+ ]);
152
262
  }
153
263
  catch (error) {
154
264
  // Only remove registration files after confirming the job is absent.
155
265
  try {
156
- await execute('launchctl', ['print', `gui/${process.getuid()}/${LABEL}`]);
266
+ await execute("launchctl", [
267
+ "print",
268
+ `gui/${process.getuid()}/${LABEL}`,
269
+ ]);
157
270
  }
158
271
  catch {
159
272
  rmSync(path);
@@ -1,13 +1,23 @@
1
- import { type IncomingMessage } from 'node:http';
2
- import { VaultCloudClient } from './cloud.js';
1
+ import { type IncomingMessage } from "node:http";
2
+ import { VaultCloudClient } from "./cloud.js";
3
+ import { authenticateWithBrowser } from "./auth.js";
4
+ import { localDevice } from "./local-device.js";
5
+ import { generateContext, listContextEngines } from "./context-engines.js";
3
6
  export declare const LOCAL_PORT = 38850;
4
7
  export declare function localAccessToken(home: string): string;
5
- type LocalCloud = Pick<VaultCloudClient, 'projects' | 'feed' | 'health'>;
8
+ type LocalCloud = Pick<VaultCloudClient, "projects" | "feed" | "health">;
6
9
  export declare function startLocalService(options?: {
7
10
  home?: string;
8
11
  port?: number;
9
12
  cloud?: LocalCloud;
10
13
  pollMs?: number;
14
+ authenticate?: typeof authenticateWithBrowser;
15
+ cacheKey?: Buffer;
16
+ device?: ReturnType<typeof localDevice> | false;
17
+ context?: {
18
+ list: () => ReturnType<typeof listContextEngines>;
19
+ generate: typeof generateContext;
20
+ };
11
21
  }): Promise<{
12
22
  server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
13
23
  port: number;