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.
- package/dist/cli-reference.md +1 -1
- package/dist/cli.md +28 -0
- package/dist/index.js +57 -12
- package/package.json +1 -1
package/dist/cli-reference.md
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
|
10150
|
-
const
|
|
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: {
|