@sipher.dev/agents 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/connector-platforms.json +14 -0
- package/connector-runtime.json +4 -0
- package/dist/index.js +319 -3400
- package/dist/local-control-DKnrc-X8.d.ts +27 -0
- package/dist/local-control-Dm6FcXAi.js +429 -0
- package/dist/local-control-server.d.ts +18 -0
- package/dist/local-control-server.js +149 -0
- package/dist/local-control.d.ts +355 -0
- package/dist/local-control.js +2 -0
- package/dist/local-machine-controller-CjOyllJj.js +453 -0
- package/dist/release-resolver-PnPDodU9.js +5828 -0
- package/dist/src-ghbvDJyd.js +731 -0
- package/package.json +32 -23
- package/release-trust-anchor.json +11 -0
- package/dist/local-machine-controller-Dtiash-C.js +0 -2129
- package/dist/local-machine.d.ts +0 -177
- package/dist/local-machine.js +0 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region ../machine-protocol/src/local-control.d.ts
|
|
2
|
+
interface LocalControlVersionRange {
|
|
3
|
+
minVersion: number;
|
|
4
|
+
maxVersion: number;
|
|
5
|
+
}
|
|
6
|
+
interface LocalControlCapability {
|
|
7
|
+
token: string;
|
|
8
|
+
ownerUserId: string;
|
|
9
|
+
deploymentId: string;
|
|
10
|
+
machineId: string;
|
|
11
|
+
lifecycleGeneration: number;
|
|
12
|
+
connectionGeneration: number;
|
|
13
|
+
activityGeneration: number;
|
|
14
|
+
candidateVersion: string;
|
|
15
|
+
policyDigest: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
}
|
|
18
|
+
interface LocalControlStatus {
|
|
19
|
+
deploymentId: string;
|
|
20
|
+
serviceKey: string;
|
|
21
|
+
machineId?: string;
|
|
22
|
+
state: "ready" | "starting" | "stopped" | "error";
|
|
23
|
+
installedVersion?: string;
|
|
24
|
+
serverVersion: null;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { LocalControlStatus as n, LocalControlVersionRange as r, LocalControlCapability as t };
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { D as discoverDeployment, _ as startInstalledConnectorService, a as installDeploymentConnector, b as resolveConnectorProfileContext, d as TokenStore, i as resolveVerifiedConnectorSnapshot, l as readConnectorRegistry, m as installConnectorLauncherService, p as connectorLauncherServiceIsRunning, r as resolveVerifiedConnectorRelease, s as withDeploymentReconciliationLock, u as removeConnectorRegistry, v as uninstallConnectorLauncherService, w as MachineClient } from "./release-resolver-PnPDodU9.js";
|
|
2
|
+
import { C as canonicalSignedConnectorReleaseTuple, G as negotiateLocalControlVersion } from "./src-ghbvDJyd.js";
|
|
3
|
+
import { readFile, readdir, rm, rmdir } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { connect } from "node:net";
|
|
7
|
+
//#region src/desktop-bootstrap.ts
|
|
8
|
+
async function bootstrapDesktopConnector(input) {
|
|
9
|
+
const platform = input.platform ?? process.platform;
|
|
10
|
+
const arch = input.arch ?? process.arch;
|
|
11
|
+
const deployment = await (input.discover ?? discoverDeployment)(input.serverUrl);
|
|
12
|
+
const context = resolveConnectorProfileContext(deployment.serviceKey, platform, input.roots);
|
|
13
|
+
try {
|
|
14
|
+
return await withDeploymentReconciliationLock({
|
|
15
|
+
serviceKey: deployment.serviceKey,
|
|
16
|
+
platform,
|
|
17
|
+
...input.roots ? { roots: input.roots } : {},
|
|
18
|
+
run: () => bootstrapDesktopConnectorLocked(input, deployment, context, platform, arch)
|
|
19
|
+
});
|
|
20
|
+
} finally {
|
|
21
|
+
await removeEmptyStateParents(context.statePath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function bootstrapDesktopConnectorLocked(input, deployment, context, platform, arch) {
|
|
25
|
+
await reconcileRetirement({
|
|
26
|
+
deployment,
|
|
27
|
+
context,
|
|
28
|
+
transport: input.transport,
|
|
29
|
+
platform,
|
|
30
|
+
...input.roots ? { roots: input.roots } : {},
|
|
31
|
+
...input.uninstall ? { uninstall: input.uninstall } : {},
|
|
32
|
+
...input.serviceRunning ? { serviceRunning: input.serviceRunning } : {}
|
|
33
|
+
});
|
|
34
|
+
const store = new TokenStore(context.authStorePath);
|
|
35
|
+
let pending = await readPendingServiceBootstrap(deployment, store);
|
|
36
|
+
if (pending && input.seed.development?.refreshInstalledRuntime === true && !snapshotMatchesSeed(pending.snapshot, input.seed)) {
|
|
37
|
+
await store.clearBootstrapReceipt(deployment.deploymentId);
|
|
38
|
+
await store.removeClientCredentials(deployment.deploymentId);
|
|
39
|
+
pending = void 0;
|
|
40
|
+
}
|
|
41
|
+
if (pending) {
|
|
42
|
+
const release = await resolvePendingBootstrapRelease(input, pending.snapshot, platform, arch);
|
|
43
|
+
try {
|
|
44
|
+
await (input.install ?? installDeploymentConnector)({
|
|
45
|
+
deployment,
|
|
46
|
+
release,
|
|
47
|
+
credentials: pending.credentials,
|
|
48
|
+
connectorInstallId: pending.connectorInstallId,
|
|
49
|
+
challenge: pending.challenge,
|
|
50
|
+
platform,
|
|
51
|
+
arch,
|
|
52
|
+
...input.roots ? { roots: input.roots } : {},
|
|
53
|
+
reconciliationLockHeld: true,
|
|
54
|
+
acknowledge: (receiptId) => input.transport.request("/api/desktop-auth/machines/bootstrap/ack", { receipt_id: receiptId })
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
serverUrl: deployment.serverUrl,
|
|
58
|
+
machineId: pending.credentials.machineId,
|
|
59
|
+
machineName: "This computer",
|
|
60
|
+
installedVersion: pending.snapshot.tuple.runtimeVersion,
|
|
61
|
+
reused: false
|
|
62
|
+
};
|
|
63
|
+
} finally {
|
|
64
|
+
await release.dispose();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const local = await discoverLocalConnector({
|
|
68
|
+
deploymentId: deployment.deploymentId,
|
|
69
|
+
serviceKey: deployment.serviceKey,
|
|
70
|
+
platform,
|
|
71
|
+
...input.roots ? { roots: input.roots } : {}
|
|
72
|
+
});
|
|
73
|
+
if (local.state === "ownership_conflict") throw codedError("ownership_conflict", "The local connector belongs to another deployment.");
|
|
74
|
+
if (local.state === "ready") {
|
|
75
|
+
if (!local.status.machineId) throw codedError("installed_connector_unavailable", "The connector is not yet registered.");
|
|
76
|
+
if (local.status.installedVersion && !versionSatisfiesCaret(local.status.installedVersion, deployment.connectorVersionRange)) throw codedError("installed_connector_incompatible", "The installed connector is outside this deployment's compatibility policy.");
|
|
77
|
+
if (!(input.seed.development?.refreshInstalledRuntime === true && local.status.installedVersion !== input.seed.snapshot.tuple.runtimeVersion)) return {
|
|
78
|
+
serverUrl: deployment.serverUrl,
|
|
79
|
+
machineId: local.status.machineId,
|
|
80
|
+
machineName: "This computer",
|
|
81
|
+
...local.status.installedVersion ? { installedVersion: local.status.installedVersion } : {},
|
|
82
|
+
reused: true
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (local.state !== "ready" && (local.entry || await readConnectorRegistry(context.registryPath))) throw codedError("installed_connector_unavailable", "The installed connector service is unavailable; Desktop will not replace a possibly busy supervisor.");
|
|
86
|
+
const release = await resolveBootstrapRelease(input, deployment, platform, arch);
|
|
87
|
+
try {
|
|
88
|
+
const prepared = await store.prepareDeployment(deployment);
|
|
89
|
+
const grant = await input.transport.request("/api/desktop-auth/machines/bootstrap/grant", {
|
|
90
|
+
deployment_id: deployment.deploymentId,
|
|
91
|
+
service_key: deployment.serviceKey,
|
|
92
|
+
connector_install_id: prepared.connectorInstallId,
|
|
93
|
+
challenge: prepared.challenge,
|
|
94
|
+
policy_digest: deployment.policyDigest,
|
|
95
|
+
release_tuple: release.snapshot.tuple
|
|
96
|
+
});
|
|
97
|
+
assertGrant(grant, deployment, prepared.connectorInstallId, release.snapshot.tuple);
|
|
98
|
+
const machineClient = new MachineClient(deployment.serverUrl);
|
|
99
|
+
const machine = await (input.claim ?? machineClient.claim.bind(machineClient))({
|
|
100
|
+
accessToken: grant.access_token,
|
|
101
|
+
deploymentId: deployment.deploymentId,
|
|
102
|
+
serviceKey: deployment.serviceKey,
|
|
103
|
+
connectorInstallId: prepared.connectorInstallId,
|
|
104
|
+
challenge: prepared.challenge,
|
|
105
|
+
bootstrap: {
|
|
106
|
+
policyDigest: deployment.policyDigest,
|
|
107
|
+
releaseTuple: release.snapshot.tuple
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (!machine.bootstrap) throw new Error("Server did not issue connector service credentials.");
|
|
111
|
+
await (input.install ?? installDeploymentConnector)({
|
|
112
|
+
deployment,
|
|
113
|
+
release,
|
|
114
|
+
credentials: {
|
|
115
|
+
...machine.bootstrap,
|
|
116
|
+
machineId: machine.machineId
|
|
117
|
+
},
|
|
118
|
+
connectorInstallId: prepared.connectorInstallId,
|
|
119
|
+
challenge: prepared.challenge,
|
|
120
|
+
platform,
|
|
121
|
+
arch,
|
|
122
|
+
...input.roots ? { roots: input.roots } : {},
|
|
123
|
+
reconciliationLockHeld: true,
|
|
124
|
+
acknowledge: (receiptId) => input.transport.request("/api/desktop-auth/machines/bootstrap/ack", { receipt_id: receiptId })
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
serverUrl: deployment.serverUrl,
|
|
128
|
+
machineId: machine.machineId,
|
|
129
|
+
machineName: machine.name,
|
|
130
|
+
installedVersion: release.snapshot.tuple.runtimeVersion,
|
|
131
|
+
reused: false
|
|
132
|
+
};
|
|
133
|
+
} finally {
|
|
134
|
+
await release.dispose();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function resolveBootstrapRelease(input, deployment, platform, arch) {
|
|
138
|
+
if (input.seed.snapshot.channel === deployment.connectorChannel && versionSatisfiesCaret(input.seed.snapshot.tuple.runtimeVersion, deployment.connectorVersionRange)) {
|
|
139
|
+
const tarball = input.seed.tarball;
|
|
140
|
+
return (input.resolveSnapshot ?? resolveVerifiedConnectorSnapshot)(input.seed.snapshot, {
|
|
141
|
+
platform,
|
|
142
|
+
arch,
|
|
143
|
+
...input.seed.verification,
|
|
144
|
+
fetch: async (url) => {
|
|
145
|
+
if (String(url) !== input.seed.snapshot.tuple.tarballUrl) return new Response("Not found", { status: 404 });
|
|
146
|
+
return new Response(Buffer.from(tarball));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return (input.resolveLive ?? resolveVerifiedConnectorRelease)(deployment, {
|
|
151
|
+
platform,
|
|
152
|
+
arch
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async function resolvePendingBootstrapRelease(input, snapshot, platform, arch) {
|
|
156
|
+
if (snapshotMatchesSeed(snapshot, input.seed)) return (input.resolveSnapshot ?? resolveVerifiedConnectorSnapshot)(snapshot, {
|
|
157
|
+
platform,
|
|
158
|
+
arch,
|
|
159
|
+
...input.seed.verification,
|
|
160
|
+
fetch: async (url) => {
|
|
161
|
+
if (String(url) !== snapshot.tuple.tarballUrl) return new Response("Not found", { status: 404 });
|
|
162
|
+
return new Response(Buffer.from(input.seed.tarball));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return (input.resolveSnapshot ?? resolveVerifiedConnectorSnapshot)(snapshot, {
|
|
166
|
+
platform,
|
|
167
|
+
arch,
|
|
168
|
+
...input.seed.verification
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function snapshotMatchesSeed(snapshot, seed) {
|
|
172
|
+
return snapshot.digest === seed.snapshot.digest && canonicalSignedConnectorReleaseTuple(snapshot.tuple) === canonicalSignedConnectorReleaseTuple(seed.snapshot.tuple);
|
|
173
|
+
}
|
|
174
|
+
async function reconcileRetirement(input) {
|
|
175
|
+
const profile = await new TokenStore(input.context.authStorePath).getProfile(input.deployment.deploymentId);
|
|
176
|
+
if (!profile) return;
|
|
177
|
+
const response = await input.transport.request("/api/desktop-auth/machines/retirement", {
|
|
178
|
+
deployment_id: input.deployment.deploymentId,
|
|
179
|
+
service_key: input.deployment.serviceKey,
|
|
180
|
+
connector_install_id: profile.connectorInstallId
|
|
181
|
+
});
|
|
182
|
+
if (response.owned !== true) throw codedError("ownership_conflict", "The installed connector belongs to another signed-in owner.");
|
|
183
|
+
if (!response.retirement) return;
|
|
184
|
+
if (input.platform !== "darwin" && input.platform !== "linux" && input.platform !== "win32") throw new Error(`Unsupported service platform: ${input.platform}.`);
|
|
185
|
+
await (input.uninstall ?? uninstallConnectorLauncherService)({
|
|
186
|
+
platform: input.platform,
|
|
187
|
+
context: input.context
|
|
188
|
+
});
|
|
189
|
+
if (await (input.serviceRunning ?? connectorLauncherServiceIsRunning)({
|
|
190
|
+
platform: input.platform,
|
|
191
|
+
context: input.context
|
|
192
|
+
})) throw new Error("Retired connector service is still running.");
|
|
193
|
+
if ((await discoverLocalConnector({
|
|
194
|
+
deploymentId: input.deployment.deploymentId,
|
|
195
|
+
serviceKey: input.deployment.serviceKey,
|
|
196
|
+
platform: input.platform,
|
|
197
|
+
...input.roots ? { roots: input.roots } : {}
|
|
198
|
+
})).state === "ready") throw new Error("Retired connector service is still running.");
|
|
199
|
+
await Promise.all([
|
|
200
|
+
removeConnectorRegistry(input.context),
|
|
201
|
+
rm(dirname(input.context.configPath), {
|
|
202
|
+
recursive: true,
|
|
203
|
+
force: true
|
|
204
|
+
}),
|
|
205
|
+
clearConnectorState(input.context)
|
|
206
|
+
]);
|
|
207
|
+
await input.transport.request("/api/desktop-auth/machines/retirement/ack", {
|
|
208
|
+
deployment_id: input.deployment.deploymentId,
|
|
209
|
+
service_key: input.deployment.serviceKey,
|
|
210
|
+
connector_install_id: profile.connectorInstallId,
|
|
211
|
+
revision: response.retirement.revision,
|
|
212
|
+
token: response.retirement.token
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function readPendingServiceBootstrap(deployment, store) {
|
|
216
|
+
const profile = await store.getProfile(deployment.deploymentId);
|
|
217
|
+
const receipt = profile?.bootstrapReceipt;
|
|
218
|
+
if (!profile?.machineId || !profile.refreshToken || !profile.accessToken || !profile.accessTokenExpiresAt || !receipt) return;
|
|
219
|
+
return {
|
|
220
|
+
connectorInstallId: profile.connectorInstallId,
|
|
221
|
+
challenge: profile.challenge,
|
|
222
|
+
snapshot: receipt.snapshot,
|
|
223
|
+
credentials: {
|
|
224
|
+
receiptId: receipt.receiptId,
|
|
225
|
+
machineId: profile.machineId,
|
|
226
|
+
refreshToken: profile.refreshToken,
|
|
227
|
+
accessToken: profile.accessToken,
|
|
228
|
+
accessTokenExpiresAt: profile.accessTokenExpiresAt
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function clearConnectorState(context) {
|
|
233
|
+
const lockName = basename(context.reconciliationLockPath);
|
|
234
|
+
const entries = await readdir(context.statePath).catch((error) => {
|
|
235
|
+
if (isCode(error, "ENOENT")) return [];
|
|
236
|
+
throw error;
|
|
237
|
+
});
|
|
238
|
+
await Promise.all(entries.filter((entry) => entry !== lockName).map((entry) => rm(join(context.statePath, entry), {
|
|
239
|
+
recursive: true,
|
|
240
|
+
force: true
|
|
241
|
+
})));
|
|
242
|
+
}
|
|
243
|
+
async function removeEmptyStateParents(statePath) {
|
|
244
|
+
const connectorsRoot = dirname(statePath);
|
|
245
|
+
const stateRoot = dirname(connectorsRoot);
|
|
246
|
+
for (const path of [
|
|
247
|
+
statePath,
|
|
248
|
+
connectorsRoot,
|
|
249
|
+
stateRoot
|
|
250
|
+
]) await rmdir(path).catch((error) => {
|
|
251
|
+
if (!isCode(error, "ENOENT") && !isCode(error, "ENOTEMPTY")) throw error;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function assertGrant(grant, deployment, connectorInstallId, tuple) {
|
|
255
|
+
if (grant.deployment_id !== deployment.deploymentId || grant.service_key !== deployment.serviceKey || grant.connector_install_id !== connectorInstallId || grant.policy_digest !== deployment.policyDigest || canonicalSignedConnectorReleaseTuple(grant.release_tuple) !== canonicalSignedConnectorReleaseTuple(tuple) || typeof grant.access_token !== "string" || !grant.access_token || !Number.isFinite(Date.parse(grant.expires_at))) throw new Error("Desktop connector bootstrap grant did not match the requested binding.");
|
|
256
|
+
}
|
|
257
|
+
function versionSatisfiesCaret(version, range) {
|
|
258
|
+
const actual = /^(\d+)\.(\d+)\.(\d+)$/.exec(version)?.slice(1).map(Number);
|
|
259
|
+
const minimum = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range)?.slice(1).map(Number);
|
|
260
|
+
if (!actual || !minimum) return false;
|
|
261
|
+
for (let index = 0; index < 3; index += 1) {
|
|
262
|
+
if (actual[index] < minimum[index]) return false;
|
|
263
|
+
if (actual[index] > minimum[index]) break;
|
|
264
|
+
}
|
|
265
|
+
return minimum[0] > 0 ? actual[0] === minimum[0] : actual[1] === minimum[1];
|
|
266
|
+
}
|
|
267
|
+
function codedError(code, message) {
|
|
268
|
+
return Object.assign(new Error(message), { code });
|
|
269
|
+
}
|
|
270
|
+
function isCode(error, code) {
|
|
271
|
+
return Boolean(error && typeof error === "object" && error.code === code);
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/local-control.ts
|
|
275
|
+
var LocalControlIncompatibleError = class extends Error {
|
|
276
|
+
code = "local_control_incompatible";
|
|
277
|
+
};
|
|
278
|
+
var LocalControlClient = class {
|
|
279
|
+
options;
|
|
280
|
+
constructor(options) {
|
|
281
|
+
this.options = options;
|
|
282
|
+
}
|
|
283
|
+
async command(command, capability) {
|
|
284
|
+
const request = {
|
|
285
|
+
type: "local-control/request",
|
|
286
|
+
id: randomUUID(),
|
|
287
|
+
command,
|
|
288
|
+
...capability ? { capability } : {}
|
|
289
|
+
};
|
|
290
|
+
const response = await this.exchange(request);
|
|
291
|
+
if (!response.ok) throw Object.assign(new Error(response.message), { code: response.code });
|
|
292
|
+
return response.status;
|
|
293
|
+
}
|
|
294
|
+
exchange(request) {
|
|
295
|
+
const versions = this.options.versions ?? {
|
|
296
|
+
minVersion: 1,
|
|
297
|
+
maxVersion: 2
|
|
298
|
+
};
|
|
299
|
+
const timeoutMs = this.options.timeoutMs ?? 5e3;
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const socket = connect(this.options.endpoint);
|
|
302
|
+
let buffer = "";
|
|
303
|
+
let accepted = false;
|
|
304
|
+
const timer = setTimeout(() => socket.destroy(/* @__PURE__ */ new Error("Local control timed out.")), timeoutMs);
|
|
305
|
+
const finish = (error, response) => {
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
socket.destroy();
|
|
308
|
+
if (error) reject(error);
|
|
309
|
+
else if (response) resolve(response);
|
|
310
|
+
};
|
|
311
|
+
socket.setEncoding("utf8");
|
|
312
|
+
socket.once("error", (error) => finish(error));
|
|
313
|
+
socket.once("connect", () => {
|
|
314
|
+
socket.write(`${JSON.stringify({
|
|
315
|
+
type: "local-control/hello",
|
|
316
|
+
secret: this.options.secret,
|
|
317
|
+
...versions
|
|
318
|
+
})}\n`);
|
|
319
|
+
});
|
|
320
|
+
socket.on("data", (chunk) => {
|
|
321
|
+
buffer += chunk;
|
|
322
|
+
while (buffer.includes("\n")) {
|
|
323
|
+
const index = buffer.indexOf("\n");
|
|
324
|
+
const line = buffer.slice(0, index);
|
|
325
|
+
buffer = buffer.slice(index + 1);
|
|
326
|
+
let message;
|
|
327
|
+
try {
|
|
328
|
+
message = JSON.parse(line);
|
|
329
|
+
} catch {
|
|
330
|
+
finish(/* @__PURE__ */ new Error("Local control returned invalid JSON."));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!accepted) {
|
|
334
|
+
if (isHelloRejected(message)) {
|
|
335
|
+
finish(message.code === "incompatible_version" ? new LocalControlIncompatibleError("Local control versions do not overlap.") : /* @__PURE__ */ new Error("Local control authentication failed."));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!isHelloAccepted(message) || !negotiateLocalControlVersion(versions, messageRange(message))) {
|
|
339
|
+
finish(/* @__PURE__ */ new Error("Local control handshake was invalid."));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
accepted = true;
|
|
343
|
+
socket.write(`${JSON.stringify(request)}\n`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (isResponse(message, request.id)) finish(void 0, message);
|
|
347
|
+
else finish(/* @__PURE__ */ new Error("Local control response was invalid."));
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
async function discoverLocalConnector(input) {
|
|
354
|
+
const context = resolveConnectorProfileContext(input.serviceKey, input.platform, input.roots);
|
|
355
|
+
const entry = await readConnectorRegistry(context.registryPath);
|
|
356
|
+
if (!entry) return { state: "needs_bootstrap" };
|
|
357
|
+
if (entry.deploymentId !== input.deploymentId || entry.serviceKey !== input.serviceKey) return {
|
|
358
|
+
state: "ownership_conflict",
|
|
359
|
+
entry
|
|
360
|
+
};
|
|
361
|
+
try {
|
|
362
|
+
const secret = (await readFile(context.controlSecretPath, "utf8")).trim();
|
|
363
|
+
const status = await new LocalControlClient({
|
|
364
|
+
endpoint: entry.endpoint,
|
|
365
|
+
secret,
|
|
366
|
+
...input.versions ? { versions: input.versions } : {}
|
|
367
|
+
}).command("status");
|
|
368
|
+
if (status.deploymentId !== input.deploymentId || status.serviceKey !== input.serviceKey) return {
|
|
369
|
+
state: "ownership_conflict",
|
|
370
|
+
entry
|
|
371
|
+
};
|
|
372
|
+
return status.state === "ready" ? {
|
|
373
|
+
state: "ready",
|
|
374
|
+
entry,
|
|
375
|
+
status
|
|
376
|
+
} : {
|
|
377
|
+
state: "needs_bootstrap",
|
|
378
|
+
entry
|
|
379
|
+
};
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof LocalControlIncompatibleError) throw error;
|
|
382
|
+
return {
|
|
383
|
+
state: "needs_bootstrap",
|
|
384
|
+
entry
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function runLocalControlCommand(input) {
|
|
389
|
+
const context = resolveConnectorProfileContext(input.serviceKey, input.platform, input.roots);
|
|
390
|
+
const entry = await readConnectorRegistry(context.registryPath);
|
|
391
|
+
if (!entry) throw Object.assign(/* @__PURE__ */ new Error("Connector service is not installed."), { code: "not_installed" });
|
|
392
|
+
if (entry.deploymentId !== input.deploymentId || entry.serviceKey !== input.serviceKey) throw Object.assign(/* @__PURE__ */ new Error("Connector service ownership conflict."), { code: "ownership_conflict" });
|
|
393
|
+
const platform = supportedServicePlatform(input.platform ?? process.platform);
|
|
394
|
+
if (input.command === "ensure-installed") await installConnectorLauncherService({
|
|
395
|
+
platform,
|
|
396
|
+
context
|
|
397
|
+
});
|
|
398
|
+
else if (input.command === "start") await startInstalledConnectorService({
|
|
399
|
+
platform,
|
|
400
|
+
context
|
|
401
|
+
});
|
|
402
|
+
const secret = (await readFile(context.controlSecretPath, "utf8")).trim();
|
|
403
|
+
const client = new LocalControlClient({
|
|
404
|
+
endpoint: entry.endpoint,
|
|
405
|
+
secret
|
|
406
|
+
});
|
|
407
|
+
return input.command === "ensure-installed" || input.command === "start" ? client.command("status") : client.command(input.command);
|
|
408
|
+
}
|
|
409
|
+
function supportedServicePlatform(platform) {
|
|
410
|
+
if (platform === "darwin" || platform === "linux" || platform === "win32") return platform;
|
|
411
|
+
throw new Error(`Unsupported service platform: ${platform}.`);
|
|
412
|
+
}
|
|
413
|
+
function isHelloAccepted(value) {
|
|
414
|
+
return Boolean(value && typeof value === "object" && value.type === "local-control/hello-accepted" && Number.isSafeInteger(value.version));
|
|
415
|
+
}
|
|
416
|
+
function isHelloRejected(value) {
|
|
417
|
+
return Boolean(value && typeof value === "object" && value.type === "local-control/hello-rejected");
|
|
418
|
+
}
|
|
419
|
+
function messageRange(message) {
|
|
420
|
+
return {
|
|
421
|
+
minVersion: message.version,
|
|
422
|
+
maxVersion: message.version
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function isResponse(value, id) {
|
|
426
|
+
return Boolean(value && typeof value === "object" && value.type === "local-control/response" && value.id === id);
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
export { bootstrapDesktopConnector as a, runLocalControlCommand as i, LocalControlIncompatibleError as n, discoverLocalConnector as r, LocalControlClient as t };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { n as LocalControlStatus, t as LocalControlCapability } from "./local-control-DKnrc-X8.js";
|
|
2
|
+
|
|
3
|
+
//#region src/local-control-server.d.ts
|
|
4
|
+
declare function startLocalControlServer(options: {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
secretPath: string;
|
|
7
|
+
status(): Promise<LocalControlStatus> | LocalControlStatus;
|
|
8
|
+
onCommand?: (command: "ensure-installed" | "start" | "restart") => Promise<void>;
|
|
9
|
+
onUpdateCommand?: (command: "retry" | "force", capability: LocalControlCapability) => Promise<void>;
|
|
10
|
+
versions?: {
|
|
11
|
+
minVersion: number;
|
|
12
|
+
maxVersion: number;
|
|
13
|
+
};
|
|
14
|
+
}): Promise<{
|
|
15
|
+
close: () => Promise<void>;
|
|
16
|
+
}>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { startLocalControlServer };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { G as negotiateLocalControlVersion, U as isLocalControlCommand, W as isLocalControlHello } from "./src-ghbvDJyd.js";
|
|
2
|
+
import { chmod, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
//#region src/local-control-server.ts
|
|
6
|
+
async function startLocalControlServer(options) {
|
|
7
|
+
const secret = (await readFile(options.secretPath, "utf8")).trim();
|
|
8
|
+
if (!secret) throw new Error("Local control secret is empty.");
|
|
9
|
+
if (!options.endpoint.startsWith("\\\\.\\pipe\\")) await rm(options.endpoint, { force: true });
|
|
10
|
+
const server = createServer((socket) => handleSocket(socket, secret, options));
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
server.once("error", reject);
|
|
13
|
+
server.listen({
|
|
14
|
+
path: options.endpoint,
|
|
15
|
+
readableAll: false,
|
|
16
|
+
writableAll: false
|
|
17
|
+
}, () => {
|
|
18
|
+
server.off("error", reject);
|
|
19
|
+
resolve();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
if (!options.endpoint.startsWith("\\\\.\\pipe\\")) await chmod(options.endpoint, 384);
|
|
23
|
+
return { close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())) };
|
|
24
|
+
}
|
|
25
|
+
function handleSocket(socket, secret, options) {
|
|
26
|
+
let buffer = "";
|
|
27
|
+
let accepted = false;
|
|
28
|
+
let pending = Promise.resolve();
|
|
29
|
+
socket.setEncoding("utf8");
|
|
30
|
+
socket.on("data", (chunk) => {
|
|
31
|
+
buffer += chunk;
|
|
32
|
+
while (buffer.includes("\n")) {
|
|
33
|
+
const index = buffer.indexOf("\n");
|
|
34
|
+
const line = buffer.slice(0, index);
|
|
35
|
+
buffer = buffer.slice(index + 1);
|
|
36
|
+
pending = pending.then(() => handleLine(line, socket, secret, options, accepted)).then((value) => {
|
|
37
|
+
accepted = value;
|
|
38
|
+
}).catch(() => {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function handleLine(line, socket, secret, options, accepted) {
|
|
45
|
+
let message;
|
|
46
|
+
try {
|
|
47
|
+
message = JSON.parse(line);
|
|
48
|
+
} catch {
|
|
49
|
+
socket.destroy();
|
|
50
|
+
return accepted;
|
|
51
|
+
}
|
|
52
|
+
if (!accepted) {
|
|
53
|
+
const supervisorVersions = options.versions ?? {
|
|
54
|
+
minVersion: 1,
|
|
55
|
+
maxVersion: 2
|
|
56
|
+
};
|
|
57
|
+
if (!isLocalControlHello(message) || !sameSecret(message.secret, secret)) {
|
|
58
|
+
send(socket, {
|
|
59
|
+
type: "local-control/hello-rejected",
|
|
60
|
+
code: "unauthorized",
|
|
61
|
+
supervisor: supervisorVersions
|
|
62
|
+
});
|
|
63
|
+
socket.end();
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const version = negotiateLocalControlVersion(message, supervisorVersions);
|
|
67
|
+
if (!version) {
|
|
68
|
+
send(socket, {
|
|
69
|
+
type: "local-control/hello-rejected",
|
|
70
|
+
code: "incompatible_version",
|
|
71
|
+
supervisor: supervisorVersions
|
|
72
|
+
});
|
|
73
|
+
socket.end();
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
send(socket, {
|
|
77
|
+
type: "local-control/hello-accepted",
|
|
78
|
+
version,
|
|
79
|
+
deploymentId: (await options.status()).deploymentId
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (!isLocalControlCommand(message)) {
|
|
84
|
+
sendResponse(socket, {
|
|
85
|
+
type: "local-control/response",
|
|
86
|
+
id: requestId(message),
|
|
87
|
+
ok: false,
|
|
88
|
+
code: "invalid_request",
|
|
89
|
+
message: "Invalid local-control request."
|
|
90
|
+
});
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (message.command === "retry" || message.command === "force") {
|
|
94
|
+
if (!options.onUpdateCommand) {
|
|
95
|
+
sendResponse(socket, {
|
|
96
|
+
type: "local-control/response",
|
|
97
|
+
id: message.id,
|
|
98
|
+
ok: false,
|
|
99
|
+
code: "not_ready",
|
|
100
|
+
message: "Connector update control is unavailable."
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
await options.onUpdateCommand(message.command, message.capability);
|
|
105
|
+
sendResponse(socket, {
|
|
106
|
+
type: "local-control/response",
|
|
107
|
+
id: message.id,
|
|
108
|
+
ok: true,
|
|
109
|
+
status: await options.status()
|
|
110
|
+
});
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (message.command !== "status") {
|
|
114
|
+
if (!options.onCommand) {
|
|
115
|
+
sendResponse(socket, {
|
|
116
|
+
type: "local-control/response",
|
|
117
|
+
id: message.id,
|
|
118
|
+
ok: false,
|
|
119
|
+
code: "not_ready",
|
|
120
|
+
message: "Local service reconciliation is unavailable."
|
|
121
|
+
});
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
await options.onCommand(message.command);
|
|
125
|
+
}
|
|
126
|
+
sendResponse(socket, {
|
|
127
|
+
type: "local-control/response",
|
|
128
|
+
id: message.id,
|
|
129
|
+
ok: true,
|
|
130
|
+
status: await options.status()
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
function sendResponse(socket, response) {
|
|
135
|
+
send(socket, response);
|
|
136
|
+
}
|
|
137
|
+
function send(socket, value) {
|
|
138
|
+
socket.write(`${JSON.stringify(value)}\n`);
|
|
139
|
+
}
|
|
140
|
+
function requestId(value) {
|
|
141
|
+
return value && typeof value === "object" && typeof value.id === "string" ? value.id : "invalid";
|
|
142
|
+
}
|
|
143
|
+
function sameSecret(candidate, expected) {
|
|
144
|
+
const left = Buffer.from(candidate);
|
|
145
|
+
const right = Buffer.from(expected);
|
|
146
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
export { startLocalControlServer };
|