@kortix/agent-tunnel 0.13.43 → 0.13.44

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/README.md CHANGED
@@ -23,6 +23,10 @@ of, in your private sessions. `--project-id` only offers "Also share with
23
23
  <project>" on the approval page; without it the computer is still yours
24
24
  everywhere.
25
25
 
26
+ Connecting the same computer again reuses its entry. The agent sends a SHA-256
27
+ hash of the hardware ID (IOPlatformUUID, `/etc/machine-id`, or `MachineGuid`);
28
+ the raw ID never leaves the computer.
29
+
26
30
  After approval, the interactive flow asks whether it should install a persistent background service. The default answer is yes.
27
31
 
28
32
  ## Run in the background
package/dist/agent-cli.js CHANGED
@@ -2971,7 +2971,7 @@ if (typeof globalThis.WebSocket === "undefined") {
2971
2971
  }
2972
2972
 
2973
2973
  // src/agent/cli.ts
2974
- import { existsSync as existsSync7, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
2974
+ import { existsSync as existsSync7, readFileSync as readFileSync10, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
2975
2975
  import { hostname as hostname4 } from "os";
2976
2976
  import { join as join12 } from "path";
2977
2977
 
@@ -3672,6 +3672,8 @@ function keepAwakeCommand(platform, pid) {
3672
3672
 
3673
3673
  // src/agent/device-auth.ts
3674
3674
  import { execFileSync, spawn as spawn2 } from "child_process";
3675
+ import { createHash as createHash2 } from "crypto";
3676
+ import { readFileSync as readFileSync4 } from "fs";
3675
3677
  import { hostname, platform } from "os";
3676
3678
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3677
3679
  var TUNNEL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -3792,12 +3794,29 @@ function machineDisplayName({
3792
3794
  return fallback;
3793
3795
  }
3794
3796
  }
3797
+ function machineId({
3798
+ os = platform(),
3799
+ run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] }),
3800
+ read = (path) => readFileSync4(path, "utf8")
3801
+ } = {}) {
3802
+ const attempt = (source) => {
3803
+ try {
3804
+ return source()?.trim() || null;
3805
+ } catch {
3806
+ return null;
3807
+ }
3808
+ };
3809
+ const raw = os === "darwin" ? attempt(() => /"IOPlatformUUID" = "([^"]+)"/.exec(run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]))?.[1]) : os === "win32" ? attempt(() => /MachineGuid\s+REG_SZ\s+(\S+)/.exec(run("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]))?.[1]) : attempt(() => read("/etc/machine-id")) ?? attempt(() => read("/var/lib/dbus/machine-id"));
3810
+ return raw ? createHash2("sha256").update(`kortix-machine:${raw.toLowerCase()}`).digest("hex") : null;
3811
+ }
3795
3812
  async function requestDeviceAuthorization(apiUrl, options = {}) {
3813
+ const id = machineId();
3796
3814
  const response = await fetch(`${apiUrl}/device-auth`, {
3797
3815
  method: "POST",
3798
3816
  headers: { "Content-Type": "application/json" },
3799
3817
  body: JSON.stringify({
3800
3818
  machineHostname: machineDisplayName(),
3819
+ ...id ? { machine_id: id } : {},
3801
3820
  ...options.projectId ? { project_id: options.projectId } : {}
3802
3821
  })
3803
3822
  });
@@ -3852,7 +3871,7 @@ function openBrowser(url) {
3852
3871
  }
3853
3872
 
3854
3873
  // src/agent/version.ts
3855
- import { readFileSync as readFileSync4 } from "fs";
3874
+ import { readFileSync as readFileSync5 } from "fs";
3856
3875
  import { dirname as dirname2, join as join4 } from "path";
3857
3876
  import { fileURLToPath } from "url";
3858
3877
  var PACKAGE_NAME = "@kortix/agent-tunnel";
@@ -3871,7 +3890,7 @@ function agentTunnelVersion() {
3871
3890
  let directory = moduleDirectory();
3872
3891
  for (let depth = 0;depth < MAX_LOOKUP_DEPTH; depth++) {
3873
3892
  try {
3874
- const manifest = JSON.parse(readFileSync4(join4(directory, "package.json"), "utf8"));
3893
+ const manifest = JSON.parse(readFileSync5(join4(directory, "package.json"), "utf8"));
3875
3894
  if (manifest?.name === PACKAGE_NAME && typeof manifest.version === "string") {
3876
3895
  const version = manifest.version;
3877
3896
  cached = version;
@@ -3979,7 +3998,7 @@ class PermissionGuard {
3979
3998
  }
3980
3999
 
3981
4000
  // src/shared/crypto.ts
3982
- import { createHash as createHash2, createHmac, timingSafeEqual, randomInt } from "crypto";
4001
+ import { createHash as createHash3, createHmac, timingSafeEqual, randomInt } from "crypto";
3983
4002
  function signMessage(signingKey, payload, nonce) {
3984
4003
  return createHmac("sha256", signingKey).update(`${nonce}:${payload}`).digest("hex");
3985
4004
  }
@@ -4069,6 +4088,7 @@ class TunnelAgent {
4069
4088
  status = null;
4070
4089
  uptime = 0;
4071
4090
  displayName;
4091
+ hardwareId = machineId();
4072
4092
  uptimeInterval = null;
4073
4093
  signingKey = null;
4074
4094
  lastNonce = 0;
@@ -4397,6 +4417,7 @@ class TunnelAgent {
4397
4417
  machineInfo: {
4398
4418
  hostname: hostname2(),
4399
4419
  displayName: this.displayName ??= machineDisplayName(),
4420
+ ...this.hardwareId ? { machineId: this.hardwareId } : {},
4400
4421
  platform: platform2(),
4401
4422
  arch: arch(),
4402
4423
  osVersion: release(),
@@ -5005,7 +5026,7 @@ function createDesktopCapability() {
5005
5026
 
5006
5027
  // src/agent/capabilities/filesystem.ts
5007
5028
  import { open, writeFile, readdir, stat, unlink, mkdir } from "fs/promises";
5008
- import { createHash as createHash3 } from "node:crypto";
5029
+ import { createHash as createHash4 } from "node:crypto";
5009
5030
 
5010
5031
  // src/shared/filesystem-validation.ts
5011
5032
  function validateFilesystemParams(method, params) {
@@ -5192,7 +5213,7 @@ function createFilesystemCapability(config) {
5192
5213
  throw new Error(`Content exceeds max size (${contentBytes} > ${maxFileSize})`);
5193
5214
  }
5194
5215
  const bytes = Buffer.from(content, encoding);
5195
- const expectedHash = createHash3("sha256").update(bytes).digest("hex");
5216
+ const expectedHash = createHash4("sha256").update(bytes).digest("hex");
5196
5217
  if (params.sha256 !== undefined && params.sha256.toLowerCase() !== expectedHash) {
5197
5218
  throw new Error("SHA-256 mismatch: content differs from the source; destination was not modified");
5198
5219
  }
@@ -5203,7 +5224,7 @@ function createFilesystemCapability(config) {
5203
5224
  const handle = await open(path, "r");
5204
5225
  try {
5205
5226
  const stats = await handle.stat();
5206
- const persistedHash = createHash3("sha256").update(await handle.readFile()).digest("hex");
5227
+ const persistedHash = createHash4("sha256").update(await handle.readFile()).digest("hex");
5207
5228
  if (persistedHash !== expectedHash)
5208
5229
  throw new Error("SHA-256 mismatch after write; destination verification failed");
5209
5230
  return {
@@ -5416,14 +5437,14 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
5416
5437
  }
5417
5438
 
5418
5439
  // src/agent/credential-store.ts
5419
- import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5440
+ import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5420
5441
  import { join as join8 } from "path";
5421
5442
  var CONFIG_DIR2 = agentTunnelHome();
5422
5443
  var CONFIG_FILE2 = join8(CONFIG_DIR2, "config.json");
5423
5444
  var PAIRING_FIELDS = ["token", "tunnelId", "enabledCapabilities"];
5424
5445
  function readConfigFile() {
5425
5446
  try {
5426
- const parsed = JSON.parse(readFileSync5(CONFIG_FILE2, "utf-8"));
5447
+ const parsed = JSON.parse(readFileSync6(CONFIG_FILE2, "utf-8"));
5427
5448
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
5428
5449
  } catch {
5429
5450
  return {};
@@ -5458,7 +5479,7 @@ function clearSavedCredentials() {
5458
5479
  return false;
5459
5480
  let existing;
5460
5481
  try {
5461
- existing = JSON.parse(readFileSync5(CONFIG_FILE2, "utf-8"));
5482
+ existing = JSON.parse(readFileSync6(CONFIG_FILE2, "utf-8"));
5462
5483
  } catch {
5463
5484
  rmSync2(CONFIG_FILE2, { force: true });
5464
5485
  return true;
@@ -5578,13 +5599,13 @@ async function promptYesNo(question, defaultValue) {
5578
5599
  }
5579
5600
 
5580
5601
  // src/agent/service.ts
5581
- import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, realpathSync as realpathSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
5602
+ import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
5582
5603
  import { platform as platform6 } from "os";
5583
5604
  import { join as join10 } from "path";
5584
5605
 
5585
5606
  // src/agent/service-drivers.ts
5586
5607
  import { spawnSync } from "child_process";
5587
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5608
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5588
5609
  import { homedir as homedir4, platform as platform5, userInfo } from "os";
5589
5610
  import { dirname as dirname5, join as join9 } from "path";
5590
5611
 
@@ -5764,7 +5785,7 @@ var launchd = {
5764
5785
  return {
5765
5786
  active: status.ok,
5766
5787
  enabled,
5767
- detail: status.detail || (installed ? readFileSync6(paths.launchdPlist, "utf8") : undefined)
5788
+ detail: status.detail || (installed ? readFileSync7(paths.launchdPlist, "utf8") : undefined)
5768
5789
  };
5769
5790
  }
5770
5791
  };
@@ -5895,7 +5916,7 @@ var scheduledTask = {
5895
5916
  return {
5896
5917
  active: state === "Running",
5897
5918
  enabled: state !== "Disabled",
5898
- detail: state || (installed ? readFileSync6(paths.windowsScript, "utf8") : undefined)
5919
+ detail: state || (installed ? readFileSync7(paths.windowsScript, "utf8") : undefined)
5899
5920
  };
5900
5921
  }
5901
5922
  };
@@ -6016,7 +6037,7 @@ function getServiceStatus() {
6016
6037
  return withDriver((driver, paths, installed) => {
6017
6038
  let upToDate = false;
6018
6039
  try {
6019
- upToDate = installed && readFileSync7(driver.unitPath(paths), "utf8") === driver.render(paths, currentRunnerParts(plannedRunner));
6040
+ upToDate = installed && readFileSync8(driver.unitPath(paths), "utf8") === driver.render(paths, currentRunnerParts(plannedRunner));
6020
6041
  } catch {}
6021
6042
  return { ...driver.status(paths, installed), upToDate };
6022
6043
  }, { installed: false, active: null });
@@ -6065,7 +6086,7 @@ function renderServiceAction(action, status) {
6065
6086
  }
6066
6087
 
6067
6088
  // src/agent/state-file.ts
6068
- import { chmodSync as chmodSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
6089
+ import { chmodSync as chmodSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync9, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
6069
6090
  import { join as join11 } from "path";
6070
6091
  function stateFilePath(home = agentTunnelHome()) {
6071
6092
  return join11(home, "state.json");
@@ -6092,7 +6113,7 @@ function writeAgentState(status, tunnel, home = agentTunnelHome()) {
6092
6113
  }
6093
6114
  function readAgentState(home = agentTunnelHome()) {
6094
6115
  try {
6095
- const parsed = JSON.parse(readFileSync8(stateFilePath(home), "utf8"));
6116
+ const parsed = JSON.parse(readFileSync9(stateFilePath(home), "utf8"));
6096
6117
  return parsed && typeof parsed === "object" && typeof parsed.status === "string" ? parsed : null;
6097
6118
  } catch {
6098
6119
  return null;
@@ -6372,7 +6393,7 @@ function commandRun(flags) {
6372
6393
  }
6373
6394
  function lastServiceActivity() {
6374
6395
  try {
6375
- const lines = readFileSync9(join12(getServicePaths().logDir, "agent-tunnel.out.log"), "utf8").split(/\r?\n/).map((line) => stripAnsi(line).trim()).filter((line) => line.length > 0);
6396
+ const lines = readFileSync10(join12(getServicePaths().logDir, "agent-tunnel.out.log"), "utf8").split(/\r?\n/).map((line) => stripAnsi(line).trim()).filter((line) => line.length > 0);
6376
6397
  return lines.at(-1) ?? null;
6377
6398
  } catch {
6378
6399
  return null;
@@ -6494,7 +6515,7 @@ function commandLogs(flags) {
6494
6515
  console.log(` ${c.dim}not created yet${c.reset}`);
6495
6516
  continue;
6496
6517
  }
6497
- const kept = readFileSync9(file, "utf8").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).filter((line) => showAll || !isShellStartupNoise(line));
6518
+ const kept = readFileSync10(file, "utf8").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).filter((line) => showAll || !isShellStartupNoise(line));
6498
6519
  const lines = collapseRepeatedLines(kept).slice(-limit);
6499
6520
  if (lines.length === 0) {
6500
6521
  console.log(` ${c.dim}empty${c.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kortix/agent-tunnel",
3
- "version": "0.13.43",
3
+ "version": "0.13.44",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/kortix-ai/suna.git",
@@ -12,7 +12,7 @@ import {
12
12
  writeAccessRequest,
13
13
  } from './access';
14
14
  import { agentTunnelHome } from './service-paths';
15
- import { machineDisplayName } from './device-auth';
15
+ import { machineDisplayName, machineId } from './device-auth';
16
16
  import { capabilityForMethod } from '../shared/permissions';
17
17
  import { TunnelErrorCode } from '../shared/types';
18
18
  import { agentTunnelVersion } from './version';
@@ -174,6 +174,8 @@ export class TunnelAgent {
174
174
  private uptime = 0;
175
175
  /** Read once: on macOS it runs `scutil`, and the pong repeats every 30 s. */
176
176
  private displayName?: string;
177
+ /** Hashed hardware id, read once (null when unreadable). */
178
+ private readonly hardwareId = machineId();
177
179
  private uptimeInterval: ReturnType<typeof setInterval> | null = null;
178
180
 
179
181
  // HMAC signature verification
@@ -576,6 +578,9 @@ export class TunnelAgent {
576
578
  machineInfo: {
577
579
  hostname: hostname(),
578
580
  displayName: (this.displayName ??= machineDisplayName()),
581
+ // Registers this machine's identity on its row, so re-pairing it
582
+ // later reuses the registration instead of adding a second one.
583
+ ...(this.hardwareId ? { machineId: this.hardwareId } : {}),
579
584
  platform: platform(),
580
585
  arch: arch(),
581
586
  osVersion: release(),
@@ -3,6 +3,7 @@ import {
3
3
  InvalidDeviceAuthResponseError,
4
4
  awaitDeviceAuthorization,
5
5
  machineDisplayName,
6
+ machineId,
6
7
  parseDeviceAuthChallenge,
7
8
  parseDeviceAuthStatus,
8
9
  requestDeviceAuthorization,
@@ -187,3 +188,64 @@ describe('machine display name', () => {
187
188
  expect(body.machineHostname).toBe(machineDisplayName());
188
189
  });
189
190
  });
191
+
192
+ describe('machine id', () => {
193
+ const HEX = /^[a-f0-9]{64}$/;
194
+ const fails = () => {
195
+ throw new Error('not available');
196
+ };
197
+ const UUID = '8B1F2C3D-4E5F-4A6B-9C7D-0E1F2A3B4C5D';
198
+
199
+ test('macOS hashes the IOPlatformUUID; the raw id never leaves the machine', () => {
200
+ const id = machineId({
201
+ os: 'darwin',
202
+ run: (command, args) => {
203
+ expect([command, ...args]).toEqual(['ioreg', '-rd1', '-c', 'IOPlatformExpertDevice']);
204
+ return `+-o J316sAP <class IOPlatformExpertDevice>\n "IOPlatformUUID" = "${UUID}"\n`;
205
+ },
206
+ read: fails,
207
+ });
208
+ expect(id).toMatch(HEX);
209
+ expect(id).not.toContain(UUID.toLowerCase().replace(/-/g, ''));
210
+ });
211
+
212
+ test('the same hardware gives the same id on every platform source, and other hardware a different one', () => {
213
+ const mac = (uuid: string) => machineId({ os: 'darwin', run: () => `"IOPlatformUUID" = "${uuid}"`, read: fails });
214
+ expect(mac(UUID)).toBe(mac(UUID));
215
+ expect(mac(UUID)).not.toBe(mac(UUID.replace('8B1F', '9B1F')));
216
+ const win = machineId({
217
+ os: 'win32',
218
+ run: () => `\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\r\n MachineGuid REG_SZ ${UUID}\r\n`,
219
+ read: fails,
220
+ });
221
+ expect(win).toBe(mac(UUID));
222
+ });
223
+
224
+ test('Linux reads /etc/machine-id, then the dbus copy', () => {
225
+ const first = machineId({ os: 'linux', run: fails, read: (path) => (path === '/etc/machine-id' ? 'abc123\n' : fails()) });
226
+ const dbus = machineId({ os: 'linux', run: fails, read: (path) => (path === '/etc/machine-id' ? fails() : 'abc123') });
227
+ expect(first).toMatch(HEX);
228
+ expect(dbus).toBe(first);
229
+ });
230
+
231
+ test('an unreadable or empty id is null, never a random one', () => {
232
+ expect(machineId({ os: 'darwin', run: fails, read: fails })).toBeNull();
233
+ expect(machineId({ os: 'darwin', run: () => 'no uuid here', read: fails })).toBeNull();
234
+ expect(machineId({ os: 'linux', run: fails, read: () => ' \n' })).toBeNull();
235
+ });
236
+
237
+ test('pairing sends the machine id', async () => {
238
+ const original = globalThis.fetch;
239
+ let body: Record<string, unknown> = {};
240
+ globalThis.fetch = (async (_url: string, init: RequestInit) => {
241
+ body = JSON.parse(String(init.body));
242
+ return new Response(JSON.stringify(challenge()), { status: 200 });
243
+ }) as unknown as typeof fetch;
244
+ try {
245
+ await requestDeviceAuthorization('https://api.kortix.com/v1/tunnel');
246
+ } finally {
247
+ globalThis.fetch = original;
248
+ }
249
+ expect(body.machine_id).toBe(machineId() ?? undefined);
250
+ });
251
+ });
@@ -1,4 +1,6 @@
1
1
  import { execFileSync, spawn } from 'child_process';
2
+ import { createHash } from 'crypto';
3
+ import { readFileSync } from 'fs';
2
4
  import { hostname, platform } from 'os';
3
5
  import { isTunnelCapability } from '../shared/permissions';
4
6
  import type { TunnelCapability } from '../shared/types';
@@ -168,6 +170,44 @@ export function machineDisplayName({
168
170
  }
169
171
  }
170
172
 
173
+ /**
174
+ * This machine's identity: sha256 of its hardware id (macOS IOPlatformUUID,
175
+ * Linux /etc/machine-id, Windows MachineGuid), so the raw id never leaves the
176
+ * machine. It survives reinstalls, renames, and network changes; the relay
177
+ * reuses the owner's existing registration of it instead of pairing a new
178
+ * one. Null when the id cannot be read.
179
+ */
180
+ export function machineId({
181
+ os = platform(),
182
+ run = (command: string, args: string[]) =>
183
+ execFileSync(command, args, { encoding: 'utf8', timeout: 2_000, stdio: ['ignore', 'pipe', 'ignore'] }),
184
+ read = (path: string) => readFileSync(path, 'utf8'),
185
+ }: {
186
+ os?: string;
187
+ run?: (command: string, args: string[]) => string;
188
+ read?: (path: string) => string;
189
+ } = {}): string | null {
190
+ const attempt = (source: () => string | undefined) => {
191
+ try {
192
+ return source()?.trim() || null;
193
+ } catch {
194
+ return null;
195
+ }
196
+ };
197
+ const raw =
198
+ os === 'darwin'
199
+ ? attempt(() => /"IOPlatformUUID" = "([^"]+)"/.exec(run('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']))?.[1])
200
+ : os === 'win32'
201
+ ? attempt(
202
+ () =>
203
+ /MachineGuid\s+REG_SZ\s+(\S+)/.exec(
204
+ run('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid']),
205
+ )?.[1],
206
+ )
207
+ : (attempt(() => read('/etc/machine-id')) ?? attempt(() => read('/var/lib/dbus/machine-id')));
208
+ return raw ? createHash('sha256').update(`kortix-machine:${raw.toLowerCase()}`).digest('hex') : null;
209
+ }
210
+
171
211
  /**
172
212
  * Starts pairing. `projectId` names the project the machine is being connected
173
213
  * to, so the approval page can skip its project picker. A relay that predates
@@ -177,12 +217,15 @@ export async function requestDeviceAuthorization(
177
217
  apiUrl: string,
178
218
  options: { projectId?: string } = {},
179
219
  ): Promise<DeviceAuthChallenge> {
220
+ const id = machineId();
180
221
  const response = await fetch(`${apiUrl}/device-auth`, {
181
222
  method: 'POST',
182
223
  headers: { 'Content-Type': 'application/json' },
183
224
  body: JSON.stringify({
184
225
  // The default machine name on the approval page.
185
226
  machineHostname: machineDisplayName(),
227
+ // One registration per machine: re-pairing reuses it.
228
+ ...(id ? { machine_id: id } : {}),
186
229
  ...(options.projectId ? { project_id: options.projectId } : {}),
187
230
  }),
188
231
  });