@sipher.dev/agents 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 };