querysub 0.699.0 → 0.700.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.
@@ -35,7 +35,8 @@
35
35
  "PowerShell(yarn upreal *)",
36
36
  "PowerShell($env:COMPUTERNAME; $env:USERNAME)",
37
37
  "PowerShell($env:COMPUTERNAME)",
38
- "PowerShell($env:USERNAME)"
38
+ "PowerShell($env:USERNAME)",
39
+ "Read(//d/repos/socket-function/src/**)"
39
40
  ]
40
41
  }
41
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.699.0",
3
+ "version": "0.700.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -1,5 +1,5 @@
1
1
  import { measureWrap } from "socket-function/src/profiling/measure";
2
- import { getIdentityCA, getMachineId, getOwnMachineId } from "sliftutils/misc/https/certs";
2
+ import { getCommonName, getIdentityCA, getMachineId, getOwnMachineId } from "sliftutils/misc/https/certs";
3
3
  import { getArchives2 } from "../-a-archives/archives2";
4
4
  import { isNode, throttleFunction, timeInHour, timeInSecond } from "socket-function/src/misc";
5
5
  import { SocketFunctionHook } from "socket-function/SocketFunctionTypes";
@@ -44,7 +44,6 @@ export const requiresNetworkTrustHook: SocketFunctionHook = async config => {
44
44
  let machineId = IdentityController_getMachineId(caller);
45
45
  let trusted = await isTrusted(machineId);
46
46
  if (!trusted) {
47
- devDebugbreak();
48
47
  let machineId = IdentityController_getMachineId(caller);
49
48
  throw new Error(`Calling machine is not trusted. Caller ${machineId} is not trusted by ${SocketFunction.mountedNodeId} to make call ${config.call.classGuid}.${config.call.functionName}. To gain trust add backblaze permissions (see hasBackblazePermissions) or set --nonetwork.`);
50
49
  }
@@ -128,6 +127,19 @@ export const loadServerCert = cache(async (machineId: string) => {
128
127
  trustCertificate(certFile);
129
128
  });
130
129
 
130
+ export async function trustMachineCertificate(config: { machineId: string; cert: Buffer }): Promise<boolean> {
131
+ let { machineId, cert } = config;
132
+ let certMachineId = getMachineId(getCommonName(cert), getDomain());
133
+ if (certMachineId !== machineId) {
134
+ throw new Error(`Expected the certificate to be for ${machineId}, it is for ${certMachineId}`);
135
+ }
136
+ if (await archives().get(machineId)) {
137
+ return false;
138
+ }
139
+ await archives().set(machineId, cert, { fallbacks: true });
140
+ return true;
141
+ }
142
+
131
143
  export const ensureWeAreTrusted = lazy(measureWrap(async () => {
132
144
  let machineKeyCert = getIdentityCA(getDomain());
133
145
  let machineId = getOwnMachineId(getDomain());
@@ -175,15 +175,16 @@ class NodeCapabilitiesControllerBase {
175
175
  return getMeasureBreakdown(range);
176
176
  }
177
177
 
178
- public async getInspectURL() {
179
- assertAssistedDebugConnectionAllowed();
178
+ public async getInspectURL(): Promise<string | undefined> {
179
+ if (!isAssistedDebugConnectionAllowed()) return undefined;
180
180
  return await getDebuggerUrl();
181
181
  }
182
182
 
183
183
  public async exposeExternalDebugPortOnce(forExternalIP: string) {
184
- assertAssistedDebugConnectionAllowed();
184
+ if (!isAssistedDebugConnectionAllowed()) return undefined;
185
185
  // https://notdevtools.com/devtools/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:62448/22895aed-f8da-4dfb-8d50-e432a9c2d827
186
186
  let debugURL = await this.getInspectURL();
187
+ if (!debugURL) return undefined;
187
188
 
188
189
  const outerUrl = new URL(debugURL);
189
190
  const wsParam = outerUrl.searchParams.get("ws");
@@ -206,10 +207,8 @@ class NodeCapabilitiesControllerBase {
206
207
  let lastExposed: (() => void) | undefined;
207
208
 
208
209
  /** The assisted debug connection (opening the inspector, forwarding the port externally, TLS proxying) only exists because public servers can't be reached directly - on a non-public server the debugger can be attached directly, and the forwarding machinery would only add attack surface. */
209
- export function assertAssistedDebugConnectionAllowed() {
210
- if (!isPublic()) {
211
- throw new Error(`Non-public server, connect directly. Don't use this assisted connection code.`);
212
- }
210
+ export function isAssistedDebugConnectionAllowed() {
211
+ return isPublic();
213
212
  }
214
213
 
215
214
 
@@ -1,6 +1,7 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, getMachineIdList, getMachineInfoLive } from "../machineSchema";
3
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, getMachineIdList, getMachineInfoLive, getMachineNameLive, setMachineNameLive } from "../machineSchema";
4
+ import { InputLabel } from "../../library-components/InputLabel";
4
5
  import { css } from "typesafecss";
5
6
  import { currentViewParam, selectedMachineIdParam, selectedServiceIdParam } from "../urlParams";
6
7
  import { formatNumber, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
@@ -29,6 +30,7 @@ export class MachineDetailPage extends qreact.Component {
29
30
 
30
31
  const machine = machineInfo;
31
32
  const isDisabled = machineConfig?.disabled || false;
33
+ const machineName = getMachineNameLive(selectedMachineId);
32
34
 
33
35
  // Get all service configs that target this machine
34
36
  let relevantServiceConfigs = new Map<string, ServiceConfig>();
@@ -65,7 +67,19 @@ export class MachineDetailPage extends qreact.Component {
65
67
 
66
68
  return <div className={css.vbox(16)}>
67
69
  <div className={css.hbox(12).pad2(16).bord2(0, 0, 20) + backgroundColor}>
68
- <h2 className={css.flexGrow(1)}>{selectedMachineId}</h2>
70
+ <div className={css.vbox(4).flexGrow(1)}>
71
+ <InputLabel
72
+ label="Name"
73
+ edit
74
+ fontSize={28}
75
+ value={machineName}
76
+ editValue={machineName || "(unnamed machine)"}
77
+ editClass={css.fontSize(28).fontWeight("bold") + (!machineName && css.colorhsl(0, 0, 45))}
78
+ tooltip="Click to name this machine"
79
+ onChangeValue={value => setMachineNameLive(selectedMachineId, value)}
80
+ />
81
+ <div className={css.fontSize(13).colorhsl(0, 0, 35)}>{selectedMachineId}</div>
82
+ </div>
69
83
  {isMachineDead && <div className={css.colorhsl(0, 80, 60)}>
70
84
  ⚠️ Machine is likely dead
71
85
  </div>}
@@ -120,7 +134,7 @@ export class MachineDetailPage extends qreact.Component {
120
134
  <ShowMore className={css.whiteSpace("pre-wrap")} maxHeight={80}>
121
135
  {(() => {
122
136
  if (typeof value === "object") {
123
- return <UsageBar label={value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
137
+ return <UsageBar label={value.label || value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
124
138
  }
125
139
  return value;
126
140
  })()}
@@ -6,7 +6,7 @@ import { isDefined } from "../../misc";
6
6
  import { formatTime } from "socket-function/src/formatting/format";
7
7
  import { css } from "typesafecss";
8
8
  import { Querysub } from "../../4-querysub/Querysub";
9
- import { MachineServiceController, getLiveServiceParameters, getMachineTargets, applyCommandTemplate, getMachineInfoLive } from "../machineSchema";
9
+ import { MachineServiceController, getLiveServiceParameters, getMachineTargets, applyCommandTemplate, getMachineInfoLive, getMachineNameLive } from "../machineSchema";
10
10
  import { FUNCTION_RUNNER_COMMAND_PREFIX } from "../serviceCategories";
11
11
 
12
12
  module.hotreload = true;
@@ -69,6 +69,7 @@ export class MachinePicker extends qreact.Component<{
69
69
  if (typeof ip !== "string") {
70
70
  ip = "";
71
71
  }
72
+ let name = getMachineNameLive(machineId);
72
73
  let sinceHeartbeat = now - machine.heartbeat;
73
74
  let isLikelyDead = sinceHeartbeat > DEAD_HEARTBEAT_THRESHOLD;
74
75
  let networks = networksPerMachine.get(machineId) || [];
@@ -86,10 +87,13 @@ export class MachinePicker extends qreact.Component<{
86
87
  }
87
88
  }}
88
89
  >
89
- <div className={css.fontSize(17).fontWeight("bold")}>
90
- {ip || machineId}
90
+ <div className={css.fontSize(17).fontWeight("bold") + (!name && css.colorhsl(0, 0, 45))}>
91
+ {name || ip || machineId}
91
92
  </div>
92
- {ip && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>
93
+ {name && ip && <div className={css.fontSize(12).colorhsl(0, 0, 30)}>
94
+ {ip}
95
+ </div>}
96
+ {(name || ip) && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>
93
97
  {machineId}
94
98
  </div>}
95
99
  {networks.length > 0 && <div className={css.fontSize(11).colorhsl(220, 60, 40)}>
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MACHINE_RESYNC_INTERVAL, MachineServiceController, getMachineInfoLive } from "../machineSchema";
3
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceController, getMachineInfoLive, getMachineNameLive } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/Querysub";
@@ -78,8 +78,11 @@ export class MachinesListPage extends qreact.Component {
78
78
  onClick={() => {
79
79
  if (!hasSelectedMachines) return;
80
80
 
81
- const machineNames = selectedMachineIds.join(", ");
82
- const confirmed = confirm(`⚠️ WARNING: This will permanently delete ${selectedMachineIds.length} machine(s):\n\n${machineNames}\n\nThis will remove all machine data and cannot be undone. Are you sure?`);
81
+ const machineDescriptions = selectedMachineIds.map(id => {
82
+ let name = getMachineNameLive(id);
83
+ return name && `${name} (${id})` || id;
84
+ }).join(", ");
85
+ const confirmed = confirm(`⚠️ WARNING: This will permanently delete ${selectedMachineIds.length} machine(s):\n\n${machineDescriptions}\n\nThis will remove all machine data and cannot be undone. Are you sure?`);
83
86
  if (!confirmed) return;
84
87
 
85
88
  Querysub.commit(() => {
@@ -125,6 +128,7 @@ export class MachinesListPage extends qreact.Component {
125
128
  const isSelected = this.state.selectedForDeletion[machineId];
126
129
  const machineConfig = machineConfigs?.find(x => x.machineId === machineId);
127
130
  const isDisabled = machineConfig?.disabled || false;
131
+ const machineName = getMachineNameLive(machineId);
128
132
 
129
133
  let failingServices = Object.keys(machineInfo.services).filter(serviceId => {
130
134
  return machineInfo.services[serviceId].errorFromLastRun;
@@ -183,11 +187,14 @@ export class MachinesListPage extends qreact.Component {
183
187
  <RenderGitRefInfo gitRef={machineInfo.gitRef} />
184
188
  {Object.values(machineInfo.info).map(value => {
185
189
  if (typeof value === "string") return undefined;
186
- return <UsageBar label={value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
190
+ return <UsageBar label={value.label || value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
187
191
  })}
188
192
  </div>
189
193
  <div className={css.vbox(4).flexGrow(1)}>
190
- <div>
194
+ <div className={css.fontSize(22).fontWeight("bold") + (!machineName && css.colorhsl(0, 0, 45))}>
195
+ {machineName || "(unnamed machine)"}
196
+ </div>
197
+ <div className={css.fontSize(12).colorhsl(0, 0, 35)}>
191
198
  {machineId}
192
199
  </div>
193
200
  <div>
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, ServiceParameters, applyCommandTemplate, getCommandTemplateVariables, getLiveServiceParameters, getMachineInfoLive, getMachineTargets, setMachineTargets, stripReleaseState } from "../machineSchema";
3
+ import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, ServiceParameters, applyCommandTemplate, getCommandTemplateVariables, getLiveServiceParameters, getMachineInfoLive, getMachineNameLive, getMachineTargets, setMachineTargets, stripReleaseState } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/Querysub";
@@ -126,10 +126,12 @@ export class ServiceDetailPage extends qreact.Component {
126
126
  ip = "";
127
127
  }
128
128
  let networks = networksPerMachine.get(machineId) || [];
129
+ let name = getMachineNameLive(machineId);
129
130
  return <div key={machineId} className={css.vbox(6).fillWidth.pad2(8, 6).hsl(0, 0, 94)}>
130
131
  <div className={css.hbox(10).alignItems("baseline")}>
131
- <div className={css.boldStyle.fontSize(15)}>{ip || machineId}</div>
132
- {ip && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>{machineId}</div>}
132
+ <div className={css.boldStyle.fontSize(20) + (!name && css.colorhsl(0, 0, 45))}>{name || ip || machineId}</div>
133
+ {name && ip && <div className={css.fontSize(13)}>{ip}</div>}
134
+ {(name || ip) && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>{machineId}</div>}
133
135
  {networks.length > 0 && <div className={css.fontSize(11).colorhsl(220, 60, 40)}>
134
136
  {networks.join(", ")}
135
137
  </div>}
@@ -237,6 +239,7 @@ export class ServiceDetailPage extends qreact.Component {
237
239
 
238
240
  return {
239
241
  machineId,
242
+ machineName: getMachineNameLive(machineId),
240
243
  variables,
241
244
  machineInfo,
242
245
  serviceInfo,
@@ -355,7 +358,7 @@ export class ServiceDetailPage extends qreact.Component {
355
358
  {/* The machines are notified on every config change and poll as a fallback, so forcing a resync had no use in practice. Kept in case that changes.
356
359
  <ResyncMachinesButton applyNodeIds={machineStatuses.map(x => x.machineInfo?.applyNodeId || "")} /> */}
357
360
  <div className={css.vbox(4).fillWidth}>
358
- {machineStatuses.map(({ machineId, variables, machineInfo, serviceInfo, isMachineDead, hasError, isDisabled, index }) => {
361
+ {machineStatuses.map(({ machineId, machineName, variables, machineInfo, serviceInfo, isMachineDead, hasError, isDisabled, index }) => {
359
362
  if (!machineInfo) return <div key={machineId}>Loading {machineId}...</div>;
360
363
 
361
364
  let backgroundColor = css.hsl(0, 0, 100); // Default: white
@@ -378,7 +381,10 @@ export class ServiceDetailPage extends qreact.Component {
378
381
  <div
379
382
  className={css.hbox(12)}
380
383
  >
381
- <div className={css.hbox(5).wrap}>
384
+ <div className={css.hbox(5).wrap.alignItems("baseline")}>
385
+ <div className={css.fontSize(20).boldStyle + (!machineName && css.colorhsl(0, 0, 45))}>
386
+ {machineName || "(unnamed machine)"}
387
+ </div>
382
388
  <div>
383
389
  {screenName}
384
390
  </div>
@@ -63,29 +63,46 @@ const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ memo
63
63
  });
64
64
 
65
65
 
66
- const getDiskInfo = measureWrap(async function getDiskInfo(): Promise<{ value: number; max: number } | undefined> {
66
+ const LARGE_DISK_MIN_BYTES = 100 * 1024 * 1024 * 1024;
67
+ const DF_LINE_REGEX = /^(\S+)\s+(\d+)\s+(\d+)\s+\d+\s+\S+\s+(.+?)\s*$/;
68
+
69
+ type DiskInfo = { device: string; mountPoint: string; value: number; max: number };
70
+
71
+ const getDiskInfos = measureWrap(async function getDiskInfos(): Promise<DiskInfo[]> {
67
72
  if (os.platform() === "win32") {
68
73
  throw new Error("Windows is not supported for machine resource monitoring");
69
74
  }
70
75
 
71
76
  try {
72
- // Linux: Get disk usage for root filesystem
73
- let result = await runPromise("df -B1 /", { quiet: true });
74
- let lines = result.split("\n");
75
- if (lines.length >= 2) {
76
- let parts = lines[1].split(/\s+/);
77
- if (parts.length >= 4) {
78
- let total = parseInt(parts[1]);
79
- let used = parseInt(parts[2]);
80
- if (total && used >= 0) {
81
- return { value: used, max: total };
77
+ let result = await runPromise("df -B1 -P", { quiet: true });
78
+ let disks: DiskInfo[] = [];
79
+ for (let line of result.split("\n").slice(1)) {
80
+ let match = DF_LINE_REGEX.exec(line);
81
+ if (!match) continue;
82
+ let [, device, totalText, usedText, mountPoint] = match;
83
+ if (!device.startsWith("/dev/") || device.startsWith("/dev/loop")) continue;
84
+ let max = parseInt(totalText);
85
+ let value = parseInt(usedText);
86
+ if (!max || !(value >= 0)) continue;
87
+ let existing = disks.find(disk => disk.device === device);
88
+ if (existing) {
89
+ if (mountPoint.length < existing.mountPoint.length) {
90
+ existing.mountPoint = mountPoint;
82
91
  }
92
+ continue;
83
93
  }
94
+ disks.push({ device, mountPoint, value, max });
84
95
  }
96
+ let large = disks.filter(disk => disk.max >= LARGE_DISK_MIN_BYTES);
97
+ if (large.length > 0) {
98
+ disks = large;
99
+ }
100
+ sort(disks, disk => -disk.max);
101
+ return disks;
85
102
  } catch (e: any) {
86
- console.warn(`Error getting disk info: ${e.message}`);
103
+ console.warn(`Error getting disk info: ${e.stack}`);
87
104
  }
88
- return undefined;
105
+ return [];
89
106
  });
90
107
 
91
108
  const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
@@ -100,9 +117,9 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
100
117
  };
101
118
 
102
119
  // Get system resource information
103
- let [memoryInfo, diskInfo] = await Promise.all([
120
+ let [memoryInfo, diskInfos] = await Promise.all([
104
121
  getMemoryInfo(),
105
- getDiskInfo()
122
+ getDiskInfos()
106
123
  ]);
107
124
 
108
125
  if (memoryInfo?.memory) {
@@ -121,9 +138,10 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
121
138
  };
122
139
  }
123
140
 
124
- if (diskInfo) {
125
- machineInfo.info.disk = {
141
+ for (let diskInfo of diskInfos) {
142
+ machineInfo.info[`disk ${diskInfo.mountPoint}`] = {
126
143
  type: "DISK",
144
+ label: `DISK ${diskInfo.mountPoint} (${diskInfo.device})`,
127
145
  value: diskInfo.value,
128
146
  max: diskInfo.max,
129
147
  };
@@ -38,6 +38,12 @@ export type MachineConfig = {
38
38
  export const machineInfos = archiveJSONT<MachineInfo>(() => getArchives2("machines/machine-heartbeats/"), { setFallbacks: true, findFallbacks: true });
39
39
  export const serviceConfigs = archiveJSONT<ServiceConfig>(() => getArchives2("machines/service-configs/"), { setFallbacks: true, findFallbacks: true });
40
40
  export const machineConfigs = archiveJSONT<MachineConfig>(() => getArchives2("machines/machine-configs/"), { setFallbacks: true, findFallbacks: true });
41
+ export const machineNames = archiveJSONT<MachineName>(() => getArchives2("machines/machine-names/"), { setFallbacks: true, findFallbacks: true });
42
+
43
+ export type MachineName = {
44
+ machineId: string;
45
+ name: string;
46
+ };
41
47
 
42
48
  export type MachineInfo = {
43
49
  machineId: string;
@@ -57,6 +63,7 @@ export type MachineInfo = {
57
63
  type: string;
58
64
  value: number;
59
65
  max: number;
66
+ label?: string;
60
67
  }>;
61
68
 
62
69
  repoUrl: string;
@@ -292,7 +299,19 @@ export class MachineServiceControllerBase {
292
299
  public async deleteMachineIds(machineIds: string[]) {
293
300
  for (let machineId of machineIds) {
294
301
  await machineInfos.delete(machineId);
302
+ await machineNames.delete(machineId);
303
+ }
304
+ }
305
+ public async getMachineNameList(): Promise<MachineName[]> {
306
+ return await machineNames.values();
307
+ }
308
+ public async setMachineName(machineId: string, name: string) {
309
+ name = name.trim();
310
+ if (!name) {
311
+ await machineNames.delete(machineId);
312
+ return;
295
313
  }
314
+ await machineNames.set(machineId, { machineId, name });
296
315
  }
297
316
  public async getMachineInfo(machineId: string): Promise<MachineInfo | undefined> {
298
317
  return await machineInfos.get(machineId);
@@ -641,6 +660,8 @@ export const MachineServiceController = getSyncedController(
641
660
  getMachineConfigList: {},
642
661
  getMachineConfig: {},
643
662
  setMachineConfig: {},
663
+ getMachineNameList: {},
664
+ setMachineName: {},
644
665
  addServiceConfig: {},
645
666
  setServiceConfigs: {},
646
667
  getServiceConfigType: {},
@@ -661,7 +682,8 @@ export const MachineServiceController = getSyncedController(
661
682
  ),
662
683
  {
663
684
  writes: {
664
- deleteMachineIds: ["MachineInfo", "MachineInfoList"],
685
+ deleteMachineIds: ["MachineInfo", "MachineInfoList", "MachineName"],
686
+ setMachineName: ["MachineName"],
665
687
  addMachineInfo: ["MachineInfo", "MachineInfoList"],
666
688
  setMachineInfo: ["MachineInfo"],
667
689
  setMachineConfig: ["MachineConfig", "MachineConfigList"],
@@ -677,6 +699,7 @@ export const MachineServiceController = getSyncedController(
677
699
  getMachineInfo: ["MachineInfo"],
678
700
  getMachineConfigList: ["MachineConfigList"],
679
701
  getMachineConfig: ["MachineConfig"],
702
+ getMachineNameList: ["MachineName"],
680
703
  getServiceList: ["ServiceConfigList"],
681
704
  getServiceConfig: ["ServiceConfig"],
682
705
  getGitInfo: ["gitInfo"],
@@ -703,3 +726,14 @@ export function getMachineInfoLive(machineId: string): MachineInfo | undefined {
703
726
  startMachineInfoRefreshLoop();
704
727
  return MachineServiceController(SocketFunction.browserNodeId()).getMachineInfo(machineId);
705
728
  }
729
+
730
+ export function getMachineNameLive(machineId: string): string {
731
+ let names = MachineServiceController(SocketFunction.browserNodeId()).getMachineNameList();
732
+ return names?.find(x => x.machineId === machineId)?.name || "";
733
+ }
734
+ export function setMachineNameLive(machineId: string, name: string) {
735
+ let controller = MachineServiceController(SocketFunction.browserNodeId());
736
+ Querysub.onCommitFinished(async () => {
737
+ await controller.setMachineName.promise(machineId, name);
738
+ });
739
+ }
@@ -7,6 +7,11 @@ import { fsExistsAsync } from "../fs";
7
7
  import { delay } from "socket-function/src/batching";
8
8
  import { SERVICE_NAME, SERVICE_UNIT_NAME } from "./machineDaemonShared";
9
9
  import { addDeployKeyToGitHub, askQuestion } from "./githubRepoAccess";
10
+ import { getOrCreateRemoteMachineId } from "sliftutils/security/machines/machines";
11
+ import { readRemoteFile, runOverSSH, SUDO_PREAMBLE } from "sliftutils/security/helpers/remoteSSH";
12
+ import { DEV_getIdentityFilePath, getMachineId, IdentityStorageType } from "sliftutils/misc/https/certs";
13
+ import { trustMachineCertificate } from "../-d-trust/NetworkTrust2";
14
+ import { getDomain } from "../config";
10
15
  // Import querysub, to fix missing dependencies
11
16
  Querysub;
12
17
 
@@ -142,6 +147,34 @@ async function installUnofficialNode(sshRemote: string, major: number): Promise<
142
147
  }
143
148
  }
144
149
 
150
+ async function setupMachineIdentity(sshRemote: string): Promise<string> {
151
+ let domain = getDomain();
152
+ let machineId = await getOrCreateRemoteMachineId(sshRemote, domain);
153
+ let home = await sshValue(sshRemote, "echo $HOME");
154
+ let identityPath = `${home}/${path.basename(DEV_getIdentityFilePath(domain))}`;
155
+ await runOverSSH({
156
+ host: sshRemote,
157
+ script: `${SUDO_PREAMBLE}\n$SUDO chown "$(id -u):$(id -g)" "${identityPath}"`,
158
+ allowFailure: true,
159
+ });
160
+ let contents = await readRemoteFile({ host: sshRemote, filePath: identityPath });
161
+ if (!contents) {
162
+ throw new Error(`Expected an identity at ${identityPath} on ${sshRemote} after creating one, there is none`);
163
+ }
164
+ let stored = JSON.parse(contents) as IdentityStorageType;
165
+ let storedMachineId = getMachineId(stored.domain, domain);
166
+ if (storedMachineId !== machineId) {
167
+ throw new Error(`Expected ${identityPath} on ${sshRemote} to hold ${machineId}, it holds ${storedMachineId}. Something else wrote an identity at the same time, so re-run setup.`);
168
+ }
169
+ let published = await trustMachineCertificate({ machineId, cert: Buffer.from(stored.certB64, "base64") });
170
+ if (published) {
171
+ console.log(`✅ Machine ${machineId} identified and added to the trust archive`);
172
+ } else {
173
+ console.log(`✅ Machine ${machineId} identified, and was already in the trust archive`);
174
+ }
175
+ return machineId;
176
+ }
177
+
145
178
  async function setupRepositoryOnRemote(sshRemote: string, gitURLLive: string, gitRefLive: string): Promise<void> {
146
179
  // Create git folder on remote
147
180
  await runPromise(`ssh ${sshRemote} "mkdir -p ~/machine-alwaysup"`);
@@ -282,6 +315,9 @@ async function main() {
282
315
  console.warn("⚠️ Backblaze file not found at:", backblazePath);
283
316
  }
284
317
 
318
+ console.log("Setting up the machine's identity and trust...");
319
+ let machineId = await setupMachineIdentity(sshRemote);
320
+
285
321
  // 2. Ensure git is installed on remote server
286
322
  console.log("Ensuring git is installed...");
287
323
  try {
@@ -293,15 +329,31 @@ async function main() {
293
329
  console.log("✅ Git installed");
294
330
  }
295
331
 
296
- // 3. Ensure build tools are installed (needed for native modules)
297
332
  console.log("Ensuring build tools are installed...");
298
- try {
299
- await runPromise(`ssh ${sshRemote} "which make"`);
300
- console.log("✅ Build tools already installed");
301
- } catch {
302
- console.log("Installing build tools...");
333
+ let buildTools = ["make", "g++", "cc"];
334
+ let missingBuildTools: string[] = [];
335
+ for (let tool of buildTools) {
336
+ let found = await runPromise(`ssh ${sshRemote} "command -v ${tool} || true"`, { nothrow: true });
337
+ if (!found.trim()) {
338
+ missingBuildTools.push(tool);
339
+ }
340
+ }
341
+ if (missingBuildTools.length === 0) {
342
+ console.log(`✅ Build tools already installed (${buildTools.join(", ")})`);
343
+ } else {
344
+ console.log(`Installing build tools (missing: ${missingBuildTools.join(", ")})...`);
303
345
  await runPromise(`ssh ${sshRemote} "sudo apt install -y build-essential"`);
304
- console.log("✅ Build tools installed");
346
+ let stillMissing: string[] = [];
347
+ for (let tool of missingBuildTools) {
348
+ let found = await runPromise(`ssh ${sshRemote} "command -v ${tool} || true"`, { nothrow: true });
349
+ if (!found.trim()) {
350
+ stillMissing.push(tool);
351
+ }
352
+ }
353
+ if (stillMissing.length > 0) {
354
+ throw new Error(`Installed build-essential on ${sshRemote}, but ${stillMissing.join(", ")} is still not on the PATH. Native modules (fs-ext, and everything else node-gyp builds) cannot compile without it - check what "sudo apt install -y build-essential" reports on that machine.`);
355
+ }
356
+ console.log(`✅ Build tools installed (${buildTools.join(", ")})`);
305
357
  }
306
358
 
307
359
  // The daemon runs everything in tmux screens, so setup cannot get by without it
@@ -488,6 +540,7 @@ sudo systemctl start ${SERVICE_UNIT_NAME}
488
540
  }
489
541
 
490
542
  console.log("✅ Machine service started!");
543
+ console.log(` Machine: ${machineId}`);
491
544
  console.log(` Screen: ssh ${sshRemote} -t "tmux attach -t ${SERVICE_NAME}"`);
492
545
  console.log(` Status: ssh ${sshRemote} "systemctl status ${SERVICE_UNIT_NAME}"`);
493
546
 
@@ -45,6 +45,10 @@ export class MachineThreadInfo extends qreact.Component<{
45
45
  handleAttach = async (threadInfo: NodeSpecialInfo | undefined) => {
46
46
  Querysub.onCommitFinished(async () => {
47
47
  const url = await NodeViewerController.nodes[SocketFunction.getBrowserNodeId()].getExternalInspectURL(threadInfo?.nodeId || "");
48
+ if (!url) {
49
+ console.log(`No assisted inspect URL for ${threadInfo?.nodeId}, connect to the debugger directly.`);
50
+ return;
51
+ }
48
52
  window.open(url, "_blank");
49
53
  });
50
54
  };
@@ -5,7 +5,7 @@ import { getAllNodeIds, getBrowserUrlNode, syncNodesNow } from "../../src/-f-nod
5
5
  import { errorToUndefined, errorToUndefinedSilent, logErrors, timeoutToError } from "../../src/errors";
6
6
  import { QuerysubController } from "../../src/4-querysub/QuerysubController";
7
7
  import { Querysub } from "../../src/4-querysub/Querysub";
8
- import { NodeCapabilitiesController, assertAssistedDebugConnectionAllowed, getControllerNodeIdList } from "../../src/-g-core-values/NodeCapabilities";
8
+ import { NodeCapabilitiesController, isAssistedDebugConnectionAllowed, getControllerNodeIdList } from "../../src/-g-core-values/NodeCapabilities";
9
9
  import { lazy } from "socket-function/src/caching";
10
10
  import { atomicObjectWrite } from "../../src/2-proxy/PathValueProxyWatcher";
11
11
  import { Button } from "../../src/library-components/Button";
@@ -274,6 +274,10 @@ export class NodeViewer extends qreact.Component {
274
274
  <button onClick={async () => {
275
275
  const controller = NodeViewerController.nodes[getBrowserUrlNode()];
276
276
  let url = await controller.getExternalInspectURL(str);
277
+ if (!url) {
278
+ console.log(`No assisted inspect URL for ${str}, connect to the debugger directly.`);
279
+ return;
280
+ }
277
281
  console.log(url);
278
282
  console.log(url.replace("https://notdevtools.com/devtools", "devtools://devtools/bundled"));
279
283
  // devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&wss=99-250-124-91.querysub.com:2058/cc5b76b2-549f-4a66-8026-021811e9edd3
@@ -354,8 +358,8 @@ class NodeViewerControllerBase {
354
358
  return (await dns.promises.lookup(callerIP)).address;
355
359
  }
356
360
 
357
- public async getExternalInspectURL(nodeId: string) {
358
- assertAssistedDebugConnectionAllowed();
361
+ public async getExternalInspectURL(nodeId: string): Promise<string | undefined> {
362
+ if (!isAssistedDebugConnectionAllowed()) return undefined;
359
363
  // TODO: Replace this with a an HTTPS => HTTP proxy, adding a secret time based key
360
364
  // - Needs to run in another process
361
365
  // - Key will use machine hash(secret key + time mod interval). Will last for 2X interval, so at
@@ -365,7 +369,9 @@ class NodeViewerControllerBase {
365
369
 
366
370
  let callerIP = getNodeIdIP(SocketFunction.getCaller().nodeId);
367
371
  let ourIP = await getExternalIP();
368
- let { externalPort, internalPort, internalInspectURL } = await NodeCapabilitiesController.nodes[nodeId].exposeExternalDebugPortOnce(ourIP);
372
+ let exposed = await NodeCapabilitiesController.nodes[nodeId].exposeExternalDebugPortOnce(ourIP);
373
+ if (!exposed) return undefined;
374
+ let { externalPort, internalPort, internalInspectURL } = exposed;
369
375
 
370
376
  const forwardCode = ((config: {
371
377
  callerIP: string;
@@ -482,8 +488,8 @@ class NodeViewerControllerBase {
482
488
  return internalInspectURL.replace(`ws=127.0.0.1:${internalPort}`, `wss=${ourDomain}:${finalPort}`);
483
489
  }
484
490
 
485
- public async getInspectURL(nodeId: string) {
486
- assertAssistedDebugConnectionAllowed();
491
+ public async getInspectURL(nodeId: string): Promise<string | undefined> {
492
+ if (!isAssistedDebugConnectionAllowed()) return undefined;
487
493
  return await NodeCapabilitiesController.nodes[nodeId].getInspectURL();
488
494
  }
489
495
 
@@ -297,6 +297,9 @@ async function attachToNode(nodeId: string): Promise<AttachNodeResult> {
297
297
  // Same machine — the inspector is reachable directly on localhost.
298
298
  mode = "local";
299
299
  const inspectUrl = await NodeCapabilitiesController.nodes[nodeId].getInspectURL();
300
+ if (!inspectUrl) {
301
+ throw new Error(`Node ${nodeId} is not public, so it has no assisted inspect URL. Connect to its debugger directly.`);
302
+ }
300
303
  const { host, port, uuidPath } = parseInspectWs(inspectUrl);
301
304
  wsUrl = `ws://${host}:${port}${uuidPath}`;
302
305
  detail =
@@ -306,8 +309,11 @@ async function attachToNode(nodeId: string): Promise<AttachNodeResult> {
306
309
  // Remote machine — have the node forward its inspector port to a
307
310
  // one-time external port locked to our IP.
308
311
  mode = "remote";
309
- const { externalPort, internalPort, internalInspectURL } =
310
- await NodeCapabilitiesController.nodes[nodeId].exposeExternalDebugPortOnce(ourIP);
312
+ const exposed = await NodeCapabilitiesController.nodes[nodeId].exposeExternalDebugPortOnce(ourIP);
313
+ if (!exposed) {
314
+ throw new Error(`Node ${nodeId} is not public, so it cannot expose an external debug port. Connect to its debugger directly.`);
315
+ }
316
+ const { externalPort, internalPort, internalInspectURL } = exposed;
311
317
  const { uuidPath } = parseInspectWs(internalInspectURL);
312
318
  wsUrl = `ws://${nodeIP}:${externalPort}${uuidPath}`;
313
319
  detail =