ghostdock 0.0.11 → 0.0.13

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.
@@ -255,7 +255,7 @@ The private network: reach your sandboxes by address and name from this machine.
255
255
 
256
256
  ### `ghostdock network connect`
257
257
 
258
- Put this machine into the private network through one provider: WireGuard and split DNS. Asks for sudo once.
258
+ Put this machine into the private network through one provider: WireGuard and split DNS. The organization (--org, else the default) becomes this machine's short-name organization. Asks for sudo once.
259
259
 
260
260
  ```
261
261
  ghostdock network connect [provider] [flags]
package/dist/cli.md CHANGED
@@ -498,6 +498,8 @@ ghostdock network connect helsinki # or name a provider (name or id)
498
498
  ghostdock network ls --org acme # sandboxes, addresses, names, and what is reachable
499
499
  curl http://quiet-harbor.acme.ghostdock.internal:8080/
500
500
  open https://quiet-harbor--acme.ghostdock.net:8080/ # in a browser: HTTPS, any port
501
+ open https://quiet-harbor.ghostdock.net:8080/ # the short name, for the connect organization
502
+ ghostdock network connect --org beta # short names now mean beta's sandboxes
501
503
  ghostdock network status
502
504
  ghostdock network disconnect
503
505
  ```
@@ -513,6 +515,10 @@ ghostdock network disconnect
513
515
  may not use answers 403.
514
516
  - **Needs `wg-quick`** (`brew install wireguard-tools`, `apt install wireguard-tools`). macOS
515
517
  and Linux. On Windows, run the CLI in WSL: then only tools inside WSL reach sandboxes.
518
+ - **Tunnel MTU 1280**, so the tunnel works on networks with a smaller path than 1500 bytes
519
+ (DS-Lite, VPNs, mobile). A config written before 2026-10-20 has 1420: `network status` and
520
+ `network ls` then say *This machine's tunnel has MTU 1420, the network now uses 1280*. Run
521
+ `ghostdock network connect` again; until then TCP works, other large packets can be lost.
516
522
  - **One key per machine**, kept in `~/.config/ghostdock/network/`.
517
523
  Only the public key is sent. The device belongs to the login it was connected with:
518
524
  `ghostdock logout` takes it out of the network, and so does not connecting for 90 days.
@@ -528,6 +534,28 @@ ghostdock network disconnect
528
534
  sandbox sees the request from `198.18.0.0`, and your device's address in
529
535
  `X-Forwarded-For`. The desktop image's Desktop opens only this way, never on the raw
530
536
  address.
537
+ - **Short names** in a browser: `https://<sandbox>.ghostdock.net:<port>` means the sandbox of
538
+ this machine's organization: the one `network connect` resolved (`--org`, `GHOSTDOCK_ORG`,
539
+ the default from `organizations use`). `connect` says which; `network status` too, and
540
+ `network ls` shows a sandbox's short name next to its full one when it is that
541
+ organization's. With one organization you need nothing else. To use another organization's
542
+ short names, connect again with `--org <other>`: there is no other way to change it, so a
543
+ short name never changes meaning behind your back (`cd` into another project does not
544
+ change it). Full names (`<sandbox>--<org>`) keep working for every organization and never
545
+ change meaning: use them in scripts, CI, `.env` files and links you share.
546
+ - **When the organization changes,** each short-name site clears its browser data once
547
+ (local storage, cache, cookies) before the other organization's sandbox sees a request, so
548
+ it never reads the first one's data. Cookies are cleared for the whole site, not only the
549
+ short name: every site under the net domain's registrable domain (`ghostdock.net`: every
550
+ sandbox app there, full names included; a net domain under the app's domain, as on staging:
551
+ the Ghostdock app too). `connect` names the site when the organization changes. A tab opened on a short name before the
552
+ change keeps the old page until you reload it: reload or close those tabs (`connect` says
553
+ so). The page *Switching organization* is that clearing and reloads by itself; *Clear this
554
+ site's data* means your browser could not clear it (Safari before 17): clear the site's
555
+ data in its settings, or use the full name.
556
+ - Tools may use short names too (`curl https://quiet-harbor.ghostdock.net:8080/`), but a
557
+ short name in a script means whatever this machine is connected with: prefer the full
558
+ name.
531
559
  - **Browsers use the HTTPS name; tools keep the raw address.** A browser that opens
532
560
  `http://quiet-harbor.acme.ghostdock.internal:3000/app` (or the raw IPv6 address) gets a
533
561
  `308` to `https://quiet-harbor--acme.ghostdock.net:3000/app`. Any other browser request on
package/dist/index.js CHANGED
@@ -8588,7 +8588,7 @@ function getCliName(argv = process.argv) {
8588
8588
  return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
8589
8589
  }
8590
8590
  // package.json
8591
- var version = "0.0.11";
8591
+ var version = "0.0.13";
8592
8592
  var package_default = {
8593
8593
  name: "ghostdock",
8594
8594
  description: "Ghostdock CLI — device login and agent tool invocation from the command line",
@@ -9958,6 +9958,16 @@ function isDnsZone(zone) {
9958
9958
  function isIPv6Address(text) {
9959
9959
  return /^[0-9a-f:]{2,39}$/.test(text) && text.includes(":");
9960
9960
  }
9961
+ async function readConfigMtu(path = configPath()) {
9962
+ let contents;
9963
+ try {
9964
+ contents = await readFile3(path, "utf8");
9965
+ } catch {
9966
+ return null;
9967
+ }
9968
+ const match = /^MTU = (\d+)$/m.exec(contents);
9969
+ return match ? Number(match[1]) : null;
9970
+ }
9961
9971
  async function writeConfig(contents, path = configPath()) {
9962
9972
  await mkdir3(dirname3(path), { recursive: true, mode: 448 });
9963
9973
  await writeFile3(path, contents, { mode: 384 });
@@ -10060,9 +10070,10 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
10060
10070
  deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
10061
10071
  return 1;
10062
10072
  }
10063
- const { device, network, provider } = registered;
10073
+ const { device, network, provider, shortNameOrganizationChanged } = registered;
10074
+ const pinned = device.shortNameOrganization;
10064
10075
  const switched = wasUp && previous?.gatewayProvider && previous.gatewayProvider.id !== provider.id ? previous.gatewayProvider : null;
10065
- printResult({ device, network, provider, interface: INTERFACE, dns }, () => [
10076
+ printResult({ device, network, provider, shortNameOrganizationChanged, interface: INTERFACE, dns }, () => [
10066
10077
  eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
10067
10078
  ...switched ? [
10068
10079
  ` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
@@ -10070,6 +10081,11 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
10070
10081
  ` Reachable: the sandboxes on ${provider.name}. One provider at a time.`,
10071
10082
  ` Names: <sandbox>.<org>.${network.zone}`,
10072
10083
  ...network.netZone ? [` In a browser: https://<sandbox>--<org>.${network.netZone}:<port>`] : [],
10084
+ ...network.netZone && pinned ? [
10085
+ ` Short names: https://<sandbox>.${network.netZone}:<port> for ${pinned.slug}'s sandboxes, on this machine.`,
10086
+ ` Another organization: ${getCliName()} network connect --org <org>.`
10087
+ ] : [],
10088
+ ...network.netZone && pinned && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : [],
10073
10089
  ...dns === "none" ? [
10074
10090
  " No split DNS on this machine (no systemd-resolved): use the addresses from",
10075
10091
  ` \`${getCliName()} network ls\`.`
@@ -10078,6 +10094,15 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
10078
10094
  ], deps.log);
10079
10095
  return 0;
10080
10096
  }
10097
+ function clearingNotice(cookieSite) {
10098
+ const site = cookieSite?.site ?? "the net domain";
10099
+ return [
10100
+ " Short names meant another organization's sandboxes before. The first short-name site each",
10101
+ ` browser opens now clears its data once: the site's storage, and every cookie of ${site}.`,
10102
+ ` That signs the browser out of every site under ${site}${cookieSite?.includesApp ? ", the Ghostdock app included" : ""}.`,
10103
+ " Reload or close browser tabs opened on short names before this connect."
10104
+ ];
10105
+ }
10081
10106
  async function runNetworkDisconnect(deps = defaultDeps()) {
10082
10107
  if (!deps.up()) {
10083
10108
  printResult({ connected: false }, () => [eventLine("item", "Not connected.")], deps.log);
@@ -10095,25 +10120,38 @@ async function readConnection(deps) {
10095
10120
  const state = await readDeviceState(deps.dir);
10096
10121
  const up = deps.up();
10097
10122
  if (!state)
10098
- return { state, up, device: null, asked: true, provider: null };
10123
+ return { state, up, device: null, asked: true, provider: null, staleMtu: null };
10099
10124
  try {
10100
- const { devices } = await deps.request("/api/v1/network/devices");
10125
+ const { devices, mtu } = await deps.request("/api/v1/network/devices");
10101
10126
  const device = devices.find((d) => d.id === state.deviceId) ?? null;
10127
+ const config = await readConfigMtu(configPath(deps.dir));
10128
+ const staleMtu = config !== null && config !== mtu ? { config, current: mtu } : null;
10129
+ if (staleMtu) {
10130
+ deps.notice(`This machine's tunnel has MTU ${staleMtu.config}, the network now uses ${staleMtu.current}. ` + `Run \`${getCliName()} network connect\` again: until then, large packets other than TCP ` + "can be lost on the way.");
10131
+ }
10102
10132
  return {
10103
10133
  state,
10104
10134
  up,
10105
10135
  device,
10106
10136
  asked: true,
10107
- provider: up ? device?.gatewayProvider ?? null : null
10137
+ provider: up ? device?.gatewayProvider ?? null : null,
10138
+ staleMtu
10108
10139
  };
10109
10140
  } catch (err) {
10110
10141
  deps.notice(`Could not ask the app about this device: ${message(err)}`);
10111
- return { state, up, device: null, asked: false, provider: up ? state.gatewayProvider : null };
10142
+ return {
10143
+ state,
10144
+ up,
10145
+ device: null,
10146
+ asked: false,
10147
+ provider: up ? state.gatewayProvider : null,
10148
+ staleMtu: null
10149
+ };
10112
10150
  }
10113
10151
  }
10114
10152
  async function runNetworkStatus(deps = defaultDeps()) {
10115
- const { state, up, device, asked, provider } = await readConnection(deps);
10116
- printResult({ connected: up, device, provider }, () => {
10153
+ const { state, up, device, asked, provider, staleMtu } = await readConnection(deps);
10154
+ printResult({ connected: up, device, provider, staleMtu }, () => {
10117
10155
  if (!state) {
10118
10156
  return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
10119
10157
  }
@@ -10123,6 +10161,9 @@ async function runNetworkStatus(deps = defaultDeps()) {
10123
10161
  ];
10124
10162
  if (device) {
10125
10163
  lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
10164
+ if (device.shortNameOrganization) {
10165
+ lines.push(` Short names: ${device.shortNameOrganization.slug}'s sandboxes.`);
10166
+ }
10126
10167
  if (!device.gatewayProvider) {
10127
10168
  lines.push(` Its provider is gone, so no gateway serves it. Run \`${getCliName()} network connect\`.`);
10128
10169
  }
@@ -10146,8 +10187,10 @@ function unreachableNote(sandbox, connected) {
10146
10187
  async function runNetworkLs(args, deps = defaultDeps()) {
10147
10188
  try {
10148
10189
  const organization = await requireOrganizationRef(args.org, deps.notice);
10149
- const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes`);
10150
- const connected = (await readConnection(deps)).provider;
10190
+ const connection = await readConnection(deps);
10191
+ const deviceId = connection.device?.id;
10192
+ const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes${deviceId ? `?device=${encodeURIComponent(deviceId)}` : ""}`);
10193
+ const connected = connection.provider;
10151
10194
  const sandboxes = listed.sandboxes.map((sandbox) => ({
10152
10195
  ...sandbox,
10153
10196
  reachable: unreachableNote(sandbox, connected) === null
@@ -10159,6 +10202,7 @@ async function runNetworkLs(args, deps = defaultDeps()) {
10159
10202
  const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
10160
10203
  const canonicalWidth = Math.max(...listed.sandboxes.map((s) => s.canonicalName.length));
10161
10204
  const httpsWidth = Math.max(...listed.sandboxes.map((s) => (s.httpsName?.length ?? 0) + 8));
10205
+ const shortWidth = Math.max(...listed.sandboxes.map((s) => (s.shortName?.length ?? 0) + 8));
10162
10206
  const lines = [
10163
10207
  connected ? eventLine("item", `Connected through ${connected.name}.`) : eventLine("item", `Not connected: nothing is reachable. Run \`${getCliName()} network connect [provider]\`.`)
10164
10208
  ];
@@ -10169,6 +10213,7 @@ async function runNetworkLs(args, deps = defaultDeps()) {
10169
10213
  s.address.padEnd(28),
10170
10214
  s.canonicalName.padEnd(canonicalWidth),
10171
10215
  ...s.httpsName ? [`https://${s.httpsName}`.padEnd(httpsWidth)] : [],
10216
+ ...s.shortName ? [`https://${s.shortName}`.padEnd(shortWidth)] : [],
10172
10217
  s.status,
10173
10218
  ...note ? [`(${note})`] : []
10174
10219
  ].join(" "));
@@ -10207,7 +10252,7 @@ async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
10207
10252
  var connectCommand = defineCommand({
10208
10253
  meta: {
10209
10254
  name: "connect",
10210
- description: "Put this machine into the private network through one provider: WireGuard and split DNS. Asks for sudo once."
10255
+ description: "Put this machine into the private network through one provider: WireGuard and split DNS. The organization (--org, else the default) becomes this machine's short-name organization. Asks for sudo once."
10211
10256
  },
10212
10257
  args: {
10213
10258
  provider: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ghostdock",
3
3
  "description": "Ghostdock CLI — device login and agent tool invocation from the command line",
4
- "version": "0.0.11",
4
+ "version": "0.0.13",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ghostdock": "dist/index.js",