@hamedb89/localghost 0.1.10 → 0.1.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/README.md +524 -34
- package/dist/cli.js +995 -93
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +343 -93
- package/dist/index.js +2051 -929
- package/dist/index.js.map +1 -1
- package/dist/{tunnel-DzfLXZ8O.d.ts → tunnel-BA52DD9e.d.ts} +53 -1
- package/dist/vite.d.ts +2 -1
- package/dist/vite.js +126 -17
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +28 -25
- package/docs/ghost-tunnel.md +206 -20
- package/docs/github.md +4 -4
- package/docs/localghost.1.md +43 -7
- package/package.json +2 -1
package/docs/flows.md
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
As a developer, I want Localghost to be a drop-in dev dependency so I can add friendly local hostnames without rebuilding the same Caddy and hosts setup in every repo.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
+
npm install -D @hamedb89/localghost
|
|
9
|
+
pnpm add -D @hamedb89/localghost
|
|
8
10
|
yarn add -D @hamedb89/localghost
|
|
11
|
+
bun add -d @hamedb89/localghost
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
Then initialize the project contract:
|
|
14
|
+
Choose the install command for the repository's package manager. Then initialize the project contract:
|
|
12
15
|
|
|
13
16
|
```sh
|
|
14
|
-
|
|
17
|
+
localghost init --write-scripts
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
## Project Contract
|
|
@@ -32,7 +35,7 @@ api.app.localhost 8787
|
|
|
32
35
|
As a developer, I want to know whether my laptop is ready before Localghost changes system files.
|
|
33
36
|
|
|
34
37
|
```sh
|
|
35
|
-
|
|
38
|
+
localghost doctor
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
Localghost checks for Caddy and prints the exact install command when it is missing. It does not run Homebrew automatically.
|
|
@@ -42,8 +45,8 @@ Localghost checks for Caddy and prints the exact install command when it is miss
|
|
|
42
45
|
As a developer, I want to know when Localghost itself is stale without paying for a network check on every run.
|
|
43
46
|
|
|
44
47
|
```sh
|
|
45
|
-
|
|
46
|
-
LOCALGHOST_NO_UPDATE_CHECK=1
|
|
48
|
+
localghost update
|
|
49
|
+
LOCALGHOST_NO_UPDATE_CHECK=1 localghost doctor
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
Localghost checks npm after successful commands, caches the result for 24 hours, and ignores check failures. `LOCALGHOST_NO_UPDATE_CHECK=1` and `--no-update-check` disable the automatic check.
|
|
@@ -53,7 +56,7 @@ Localghost checks npm after successful commands, caches the result for 24 hours,
|
|
|
53
56
|
As a developer, I want one explicit setup command that updates only the managed Localghost block in `/etc/hosts` and validates Caddy.
|
|
54
57
|
|
|
55
58
|
```sh
|
|
56
|
-
|
|
59
|
+
localghost setup
|
|
57
60
|
```
|
|
58
61
|
|
|
59
62
|
## Daily Dev
|
|
@@ -61,8 +64,8 @@ yarn localghost:setup
|
|
|
61
64
|
As a developer, I want a daily command that starts the local HTTP proxy from the same config file, with local HTTPS available only when I ask for it.
|
|
62
65
|
|
|
63
66
|
```sh
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
localghost status --ready
|
|
68
|
+
localghost dev
|
|
66
69
|
```
|
|
67
70
|
|
|
68
71
|
Most repos will run this next to their app server, for example Vite on `127.0.0.1:5173`.
|
|
@@ -70,7 +73,7 @@ Most repos will run this next to their app server, for example Vite on `127.0.0.
|
|
|
70
73
|
When a repo really needs local certificates:
|
|
71
74
|
|
|
72
75
|
```sh
|
|
73
|
-
|
|
76
|
+
localghost dev --https
|
|
74
77
|
```
|
|
75
78
|
|
|
76
79
|
## Config Discovery
|
|
@@ -78,9 +81,9 @@ yarn localghost:proxy:https
|
|
|
78
81
|
As a developer, I want Localghost to fit repos that already have naming conventions without hidden file searches.
|
|
79
82
|
|
|
80
83
|
```sh
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
localghost print --config .localghost.preview
|
|
85
|
+
localghost print --config .localghost.private --config .localghost
|
|
86
|
+
localghost print --config-pattern '^\.localghost\.(private|preview)$'
|
|
84
87
|
```
|
|
85
88
|
|
|
86
89
|
Localghost uses the first existing configured file. Regex discovery scans filenames in the project root.
|
|
@@ -90,7 +93,7 @@ Localghost uses the first existing configured file. Regex discovery scans filena
|
|
|
90
93
|
As a developer, I want to see the local domain layer as a simple `domain -> upstream` map.
|
|
91
94
|
|
|
92
95
|
```sh
|
|
93
|
-
|
|
96
|
+
localghost routes
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
```txt
|
|
@@ -115,7 +118,7 @@ export default {
|
|
|
115
118
|
|
|
116
119
|
The plugin defaults to HTTP. Pass `https: true` only when Vite is expected to sit behind a Caddy HTTPS proxy. Localghost prints URLs but does not open browser tabs.
|
|
117
120
|
|
|
118
|
-
If `.localghost` is missing, an interactive
|
|
121
|
+
If `.localghost` is missing, an interactive development run asks whether to create it, asks for the primary `.localhost` domain, allows extra domains, explains the `/etc/hosts` password prompt, and runs setup when confirmed. Non-interactive runs fail with the exact setup command instead of guessing.
|
|
119
122
|
|
|
120
123
|
## Ghost Tunnel
|
|
121
124
|
|
|
@@ -124,13 +127,13 @@ As a production app, I want one opt-in flag that makes `<route>-<project>-<owner
|
|
|
124
127
|
```js
|
|
125
128
|
export default defineLocalghostConfig({
|
|
126
129
|
ghostTunnel: {
|
|
127
|
-
domains: "
|
|
130
|
+
domains: "moonlit-otter.example",
|
|
128
131
|
mode: "manual"
|
|
129
132
|
}
|
|
130
133
|
});
|
|
131
134
|
```
|
|
132
135
|
|
|
133
|
-
The production flow is DNS wildcard -> `constructGhostTunnelUrl` -> deployed app
|
|
136
|
+
The production flow is DNS wildcard -> same-project ingress handler -> `constructGhostTunnelUrl` -> deployed app auth -> `assertSecureGhostTunnelRequest` -> exact `.ghosttunnel` lookup -> transport decision. By default the helper constructs and parses `route`, `project`, and `owner` from the wildcard label before `ghost.<domain>`, requires HTTPS, and requires the app to pass `authenticated: true`. `transport: "none"` stops at ingress plus lookup, `transport: "ip"` accepts a signed direct-address token and redirects to that address plus the exact `.ghosttunnel` port, and `transport: "tunnel"` queues the request through Redis for a local `localghost tunnel` agent. See [Ghost Tunnel](./ghost-tunnel.md).
|
|
134
137
|
|
|
135
138
|
Relay registration is local-agent-only: signed exact-host claims, explicit local targets, private access by default, no arbitrary URL proxy endpoint, and safe offline behavior when the agent disconnects.
|
|
136
139
|
|
|
@@ -141,8 +144,8 @@ When `ghostTunnel` is configured, route and Vite startup logs print the producti
|
|
|
141
144
|
As a developer, I want to retest setup without deleting my project config.
|
|
142
145
|
|
|
143
146
|
```sh
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
localghost reset
|
|
148
|
+
localghost setup
|
|
146
149
|
```
|
|
147
150
|
|
|
148
151
|
`reset` removes only the managed hosts block, generated Caddyfile, and setup state. It leaves `.localghost` in place.
|
|
@@ -152,8 +155,8 @@ yarn localghost setup
|
|
|
152
155
|
As a developer, I want to cleanly remove Localghost from a project when the repo is archived or no longer needs friendly hostnames.
|
|
153
156
|
|
|
154
157
|
```sh
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
localghost teardown
|
|
159
|
+
localghost teardown --remove-caddyfile
|
|
157
160
|
```
|
|
158
161
|
|
|
159
162
|
`teardown` removes only the Localghost managed `/etc/hosts` block. The generated Caddyfile is kept unless `--remove-caddyfile` is passed.
|
|
@@ -163,8 +166,8 @@ yarn localghost teardown --remove-caddyfile
|
|
|
163
166
|
As a developer or agent, I want to see what Localghost changed without reading system files directly.
|
|
164
167
|
|
|
165
168
|
```sh
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
localghost status
|
|
170
|
+
localghost status --json
|
|
168
171
|
```
|
|
169
172
|
|
|
170
173
|
Localghost records setup and teardown in `ops/local/localghost-state.json`. That file is project-local state, not OS temp state.
|
|
@@ -174,9 +177,9 @@ Localghost records setup and teardown in `ops/local/localghost-state.json`. That
|
|
|
174
177
|
As a Codex or agent user, I want commands that are inspectable and scriptable without opening a browser.
|
|
175
178
|
|
|
176
179
|
```sh
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
localghost print
|
|
181
|
+
localghost doctor
|
|
182
|
+
localghost update
|
|
180
183
|
```
|
|
181
184
|
|
|
182
185
|
The CLI reference lives in [localghost(1)](./localghost.1.md). Future flows can add MCP helpers and repo templates, but the base package should remain a small, predictable CLI.
|
package/docs/ghost-tunnel.md
CHANGED
|
@@ -9,7 +9,7 @@ Ghost Tunnel is the production-facing Localghost entrypoint for apps that want a
|
|
|
9
9
|
For Social Workouts, the default namespace is:
|
|
10
10
|
|
|
11
11
|
```txt
|
|
12
|
-
<route>-<project>-<owner>.ghost.
|
|
12
|
+
<route>-<project>-<owner>.ghost.moonlit-otter.example
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
The feature is off by default. Opt in from `localghost.config.mjs`:
|
|
@@ -19,8 +19,10 @@ import { defineLocalghostConfig } from "@hamedb89/localghost";
|
|
|
19
19
|
|
|
20
20
|
export default defineLocalghostConfig({
|
|
21
21
|
ghostTunnel: {
|
|
22
|
-
domains: "
|
|
23
|
-
mode: "manual"
|
|
22
|
+
domains: "moonlit-otter.example",
|
|
23
|
+
mode: "manual",
|
|
24
|
+
adapter: "vercel",
|
|
25
|
+
transport: "none"
|
|
24
26
|
}
|
|
25
27
|
});
|
|
26
28
|
```
|
|
@@ -30,7 +32,7 @@ With `ghostTunnel: { domains }` in the default manual mode, route output and Vit
|
|
|
30
32
|
```txt
|
|
31
33
|
localghost ghost tunnel
|
|
32
34
|
mode: manual
|
|
33
|
-
expected: https://app-decision-layer-hamed.ghost.
|
|
35
|
+
expected: https://app-decision-layer-hamed.ghost.moonlit-otter.example/
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
Without `domains`, the log keeps the production domain wildcarded with `*`:
|
|
@@ -48,7 +50,52 @@ Public mode keeps the namespace flexible unless you configure a preview. This is
|
|
|
48
50
|
```txt
|
|
49
51
|
localghost ghost tunnel
|
|
50
52
|
mode: public
|
|
51
|
-
configured: https://<route>-<project>-<owner>.ghost.
|
|
53
|
+
configured: https://<route>-<project>-<owner>.ghost.moonlit-otter.example/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The adapter describes where the wildcard ingress runs. The transport describes how that deployed ingress reaches your local machine. The lowest-risk smoke path is the same-project Vercel adapter with no public-to-local transport:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
export default defineLocalghostConfig({
|
|
60
|
+
ghostTunnel: {
|
|
61
|
+
domains: "copper-comet.example",
|
|
62
|
+
mode: "public",
|
|
63
|
+
requireAuth: false,
|
|
64
|
+
adapter: {
|
|
65
|
+
provider: "vercel",
|
|
66
|
+
strategy: "same-project"
|
|
67
|
+
},
|
|
68
|
+
transport: "none"
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`transport: "none"` means the wildcard host is intercepted by the deployed app and resolved against `.ghosttunnel`, but no live public-to-local stream is established yet. This is the right first slice for proving routing, exact-host lookup, and response semantics before adding a real transport.
|
|
74
|
+
|
|
75
|
+
The split is intentional:
|
|
76
|
+
|
|
77
|
+
- `adapter`: where ingress lives, for example the same Vercel project or a separate relay app.
|
|
78
|
+
- `transport`: how public traffic gets back to your local machine.
|
|
79
|
+
|
|
80
|
+
Today `transport: "none"`, `transport: "ip"`, and `transport: "tunnel"` are implemented. The `tunnel` transport is the drop-in shared-state version: deployed ingress queues the request in Redis, the local agent polls that queue, forwards to `127.0.0.1:<port>`, and writes the response back.
|
|
81
|
+
|
|
82
|
+
`transport: "ip"` is the thinnest direct-address transport. The deployed ingress still validates the ghost host and resolves the exact route from `.ghosttunnel`, but the shared URL also carries a signed IP transport token. The handler then redirects to that address plus the configured local port. This keeps the route and port in repo truth while making the shared address explicit.
|
|
83
|
+
|
|
84
|
+
For LAN or private-address use, opt in explicitly:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
export default defineLocalghostConfig({
|
|
88
|
+
ghostTunnel: {
|
|
89
|
+
domains: "copper-comet.example",
|
|
90
|
+
mode: "public",
|
|
91
|
+
requireAuth: false,
|
|
92
|
+
adapter: "vercel",
|
|
93
|
+
transport: {
|
|
94
|
+
kind: "ip",
|
|
95
|
+
allowPrivateNetworkAddress: true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
52
99
|
```
|
|
53
100
|
|
|
54
101
|
Use object form to override defaults or provide a concrete preview URL:
|
|
@@ -56,7 +103,7 @@ Use object form to override defaults or provide a concrete preview URL:
|
|
|
56
103
|
```js
|
|
57
104
|
export default defineLocalghostConfig({
|
|
58
105
|
ghostTunnel: {
|
|
59
|
-
domains: "
|
|
106
|
+
domains: "moonlit-otter.example",
|
|
60
107
|
preview: {
|
|
61
108
|
route: "plan",
|
|
62
109
|
project: "summer-base",
|
|
@@ -71,16 +118,141 @@ export default defineLocalghostConfig({
|
|
|
71
118
|
1. Add `ghostTunnel: { domains: "your-domain.com" }`, `ghostTunnel: "manual"`, or `ghostTunnel.preview` to `localghost.config.mjs`.
|
|
72
119
|
2. Point the wildcard DNS record for `*.ghost.<your-domain>` at the deployed app.
|
|
73
120
|
3. Route `*.ghost.<your-domain>` to the same production app that serves the Vite build.
|
|
74
|
-
4.
|
|
75
|
-
5.
|
|
76
|
-
6.
|
|
121
|
+
4. Add a same-project ingress handler that intercepts the wildcard host before the static app shell.
|
|
122
|
+
5. In that handler, read the Localghost project config without resolving local `.localghost` setup.
|
|
123
|
+
6. Construct tunnel URLs from `route`, `project`, and `owner`.
|
|
124
|
+
7. Validate the incoming request host, protocol, and auth before serving the tunnel surface.
|
|
125
|
+
8. For `transport: "tunnel"`, provide Redis REST env vars to the deployed handler and run `localghost tunnel` beside the local dev server.
|
|
126
|
+
|
|
127
|
+
## Exact Route File
|
|
128
|
+
|
|
129
|
+
Use `.ghosttunnel` as the exact-host companion to `.localghost`:
|
|
130
|
+
|
|
131
|
+
```txt
|
|
132
|
+
decisionlayer-decision-layer-hamedbahrami.ghost.copper-comet.example 5173
|
|
133
|
+
notes-decision-layer-hamedbahrami.ghost.copper-comet.example 4173
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The format matches `.localghost`: one exact host and one local port per line. Localghost exposes `readGhostTunnelEntries()`, `findGhostTunnelEntry()`, and `resolveGhostTunnelRequest()` so a same-project ingress handler can parse the wildcard host and look up its exact local target without inventing a second config format.
|
|
137
|
+
|
|
138
|
+
## Same-Project Vercel Handler
|
|
139
|
+
|
|
140
|
+
For a drop-in Vite app on Vercel, connect `*.ghost.<your-domain>` to the same project, then rewrite ghost hosts into a function instead of the static app:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"rewrites": [
|
|
145
|
+
{
|
|
146
|
+
"source": "/:path*",
|
|
147
|
+
"has": [{ "type": "host", "value": "(.*)\\.ghost\\.copper-comet\\.example" }],
|
|
148
|
+
"destination": "/api/ghost"
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Then the handler can resolve the request and return a safe relay status, a signed IP redirect, or a Redis-backed tunnel response:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { createVercelGhostTunnelHandler } from "@hamedb89/localghost";
|
|
158
|
+
|
|
159
|
+
export default createVercelGhostTunnelHandler({
|
|
160
|
+
cwd: process.cwd(),
|
|
161
|
+
domain: "copper-comet.example",
|
|
162
|
+
authenticated: false,
|
|
163
|
+
ipSigningSecret: process.env.LOCALGHOST_IP_SIGNING_SECRET
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`ipSigningSecret` is only required for `transport: "ip"`. For `transport: "tunnel"`, the handler uses the Redis REST env vars above unless you pass a custom `tunnelStore`.
|
|
168
|
+
|
|
169
|
+
## Signed IP URLs
|
|
170
|
+
|
|
171
|
+
Use `constructGhostTunnelIpUrl()` to create a shareable URL for `transport: "ip"`:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { constructGhostTunnelIpUrl } from "@hamedb89/localghost";
|
|
175
|
+
|
|
176
|
+
const url = constructGhostTunnelIpUrl({
|
|
177
|
+
domain: "copper-comet.example",
|
|
178
|
+
route: "decisionlayer",
|
|
179
|
+
project: "decision-layer",
|
|
180
|
+
owner: "hamedbahrami",
|
|
181
|
+
path: "/week/1",
|
|
182
|
+
address: "203.0.113.10",
|
|
183
|
+
signingSecret: process.env.LOCALGHOST_IP_SIGNING_SECRET!,
|
|
184
|
+
ghostTunnel: {
|
|
185
|
+
mode: "public",
|
|
186
|
+
domains: "copper-comet.example",
|
|
187
|
+
transport: "ip"
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
That shared URL still uses the wildcard ghost host, but it also carries a signed `__localghost=...` token. The deployed handler verifies that token against the exact ghost host, reads the local port from `.ghosttunnel`, and redirects to the direct address:
|
|
193
|
+
|
|
194
|
+
```txt
|
|
195
|
+
https://decisionlayer-decision-layer-hamedbahrami.ghost.copper-comet.example/week/1?__localghost=...
|
|
196
|
+
-> http://203.0.113.10:5173/week/1
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This is intentionally simple and explicit:
|
|
200
|
+
|
|
201
|
+
- the repo owns the route and port through `.ghosttunnel`
|
|
202
|
+
- the shared link owns the direct address
|
|
203
|
+
- the deployed handler only accepts signed IP claims bound to the exact ghost host
|
|
204
|
+
|
|
205
|
+
It is not a hidden reverse proxy. After redirect, the browser talks directly to the target address.
|
|
206
|
+
|
|
207
|
+
## Redis Tunnel Transport
|
|
208
|
+
|
|
209
|
+
Use `transport: "tunnel"` when you want the shared URL to keep going through the deployed Ghost Tunnel host instead of redirecting the browser to a direct IP address:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
export default defineLocalghostConfig({
|
|
213
|
+
ghostTunnel: {
|
|
214
|
+
domains: "copper-comet.example",
|
|
215
|
+
mode: "public",
|
|
216
|
+
requireAuth: false,
|
|
217
|
+
adapter: "vercel",
|
|
218
|
+
transport: {
|
|
219
|
+
kind: "tunnel",
|
|
220
|
+
store: {
|
|
221
|
+
provider: "vercel-redis",
|
|
222
|
+
env: "auto"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`env: "auto"` reads Redis REST variables from the deployed function environment. The current lookup order is:
|
|
230
|
+
|
|
231
|
+
```txt
|
|
232
|
+
LOCALGHOST_REDIS_REST_URL / LOCALGHOST_REDIS_REST_TOKEN
|
|
233
|
+
UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN
|
|
234
|
+
KV_REST_API_URL / KV_REST_API_TOKEN
|
|
235
|
+
REDIS_REST_API_URL / REDIS_REST_API_TOKEN
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For Vercel, add a Redis Marketplace integration to the project and make sure it exposes REST URL/token variables to the Production environment. Legacy Vercel KV REST variables still work, but new projects should treat Redis as the current provider shape.
|
|
239
|
+
|
|
240
|
+
Run the local agent next to your dev server:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
localghost tunnel
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The agent reads `localghost.config.mjs`, reads exact hosts from `.ghosttunnel`, sends short route heartbeats to Redis, polls for queued requests, and serves them from local ports. It does not open arbitrary targets; each public host must exist in `.ghosttunnel`.
|
|
247
|
+
|
|
248
|
+
The MVP transport is polling-based and intentionally bounded. It is good for app previews and demos, not large uploads, streaming responses, or high-throughput production traffic. Defaults are a 25 second ingress wait window, 30 second route heartbeat, 60 second request TTL, 1 MiB request body cap, and 5 MiB response body cap. Override those on `transport` only when the app needs it.
|
|
77
249
|
|
|
78
250
|
## Vercel DNS
|
|
79
251
|
|
|
80
252
|
For Vercel, add the Ghost Tunnel wildcard domain to the project first:
|
|
81
253
|
|
|
82
254
|
```txt
|
|
83
|
-
*.ghost.
|
|
255
|
+
*.ghost.copper-comet.example
|
|
84
256
|
```
|
|
85
257
|
|
|
86
258
|
Then create the DNS record that points that wildcard at the same Vercel project. In Vercel-managed DNS, the record name is relative to the zone:
|
|
@@ -109,7 +281,7 @@ Type: CNAME
|
|
|
109
281
|
Value: cname.vercel-dns-016.com.
|
|
110
282
|
```
|
|
111
283
|
|
|
112
|
-
Do not use only `*` for Ghost Tunnel. A record named `*` covers `anything.
|
|
284
|
+
Do not use only `*` for Ghost Tunnel. A record named `*` covers `anything.copper-comet.example`, but Ghost Tunnel URLs look like `decisionlayer-decision-layer-hamed.ghost.copper-comet.example`, so the wildcard must live under `ghost`.
|
|
113
285
|
|
|
114
286
|
```ts
|
|
115
287
|
import {
|
|
@@ -121,7 +293,7 @@ import {
|
|
|
121
293
|
const { config } = await readLocalghostProjectConfig();
|
|
122
294
|
|
|
123
295
|
const url = constructGhostTunnelUrl({
|
|
124
|
-
domain: "
|
|
296
|
+
domain: "moonlit-otter.example",
|
|
125
297
|
route: "plan",
|
|
126
298
|
project: "summer-base",
|
|
127
299
|
owner: "hamed",
|
|
@@ -130,13 +302,13 @@ const url = constructGhostTunnelUrl({
|
|
|
130
302
|
|
|
131
303
|
const route = assertSecureGhostTunnelRequest({
|
|
132
304
|
host: request.headers.get("host") ?? "",
|
|
133
|
-
domain: "
|
|
305
|
+
domain: "moonlit-otter.example",
|
|
134
306
|
protocol: request.url.startsWith("https:") ? "https" : "http",
|
|
135
307
|
authenticated: Boolean(session),
|
|
136
308
|
ghostTunnel: config.ghostTunnel
|
|
137
309
|
});
|
|
138
310
|
|
|
139
|
-
// url is https://plan-summer-base-hamed.ghost.
|
|
311
|
+
// url is https://plan-summer-base-hamed.ghost.moonlit-otter.example/
|
|
140
312
|
// route.namespace is { route: "plan", project: "summer-base", owner: "hamed" }.
|
|
141
313
|
```
|
|
142
314
|
|
|
@@ -145,7 +317,7 @@ When `ghostTunnel.preview` is configured, Localghost logs the concrete preview U
|
|
|
145
317
|
```txt
|
|
146
318
|
localghost ghost tunnel
|
|
147
319
|
mode: manual
|
|
148
|
-
expected: https://plan-summer-base-hamed.ghost.
|
|
320
|
+
expected: https://plan-summer-base-hamed.ghost.moonlit-otter.example/
|
|
149
321
|
```
|
|
150
322
|
|
|
151
323
|
In an interactive Vite terminal, press `g` to show the Ghost Tunnel configuration and open a numbered concrete URL. Wildcard `*` URLs are shown for observability, but the menu only opens configured concrete domains.
|
|
@@ -159,7 +331,7 @@ The default namespace tags are `route`, `project`, and `owner`, joined with `-`.
|
|
|
159
331
|
```js
|
|
160
332
|
export default defineLocalghostConfig({
|
|
161
333
|
ghostTunnel: {
|
|
162
|
-
domains: "
|
|
334
|
+
domains: "moonlit-otter.example"
|
|
163
335
|
}
|
|
164
336
|
});
|
|
165
337
|
```
|
|
@@ -194,7 +366,7 @@ For custom tags, pass extra values to the constructor:
|
|
|
194
366
|
|
|
195
367
|
```ts
|
|
196
368
|
constructGhostTunnelUrl({
|
|
197
|
-
domain: "
|
|
369
|
+
domain: "moonlit-otter.example",
|
|
198
370
|
route: "plan",
|
|
199
371
|
project: "summer-base",
|
|
200
372
|
owner: "hamed",
|
|
@@ -209,7 +381,7 @@ constructGhostTunnelUrl({
|
|
|
209
381
|
|
|
210
382
|
- `ghostTunnel` is opt-in and resolves to disabled unless the project config enables it.
|
|
211
383
|
- The default production entry host is `ghost.<your-domain>`, with a wildcard of `*.ghost.<your-domain>`.
|
|
212
|
-
- The default wildcard label must be `route-project-owner`, such as `plan-summer-base-hamed.ghost.
|
|
384
|
+
- The default wildcard label must be `route-project-owner`, such as `plan-summer-base-hamed.ghost.moonlit-otter.example`.
|
|
213
385
|
- The configured spread tag may contain the namespace separator. By default, that is `project`.
|
|
214
386
|
- Other namespace values cannot include the namespace separator, because parsing must be reversible.
|
|
215
387
|
- Host labels must be DNS-safe lowercase ASCII labels after normalization.
|
|
@@ -219,7 +391,7 @@ constructGhostTunnelUrl({
|
|
|
219
391
|
|
|
220
392
|
## Relay Security
|
|
221
393
|
|
|
222
|
-
Localghost relay is private by default. Public requests can select a Ghost Tunnel route, but they must never select
|
|
394
|
+
Localghost relay is private by default. Public requests can select a Ghost Tunnel route, but they must never select an arbitrary local target URL or port. There must be no `/proxy?url=...` style endpoint.
|
|
223
395
|
|
|
224
396
|
Route registration goes through an authenticated local agent:
|
|
225
397
|
|
|
@@ -230,7 +402,7 @@ import {
|
|
|
230
402
|
} from "@hamedb89/localghost";
|
|
231
403
|
|
|
232
404
|
const claim = signRelayRouteClaim({
|
|
233
|
-
host: "plan-summer-base-hamed.ghost.
|
|
405
|
+
host: "plan-summer-base-hamed.ghost.moonlit-otter.example",
|
|
234
406
|
scope: "socialworkouts:preview",
|
|
235
407
|
agentId: "local-agent-1",
|
|
236
408
|
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString()
|
|
@@ -264,6 +436,20 @@ The relay helpers enforce these rules:
|
|
|
264
436
|
- `renderRelayOfflineResponse()` returns a safe offline page with no secrets or stack traces.
|
|
265
437
|
- Vite integration continues to generate explicit `allowedHosts`; it never sets `allowedHosts: true`.
|
|
266
438
|
|
|
439
|
+
For `transport: "ip"`, the handler uses a signed IP token instead of an open target selector:
|
|
440
|
+
|
|
441
|
+
- `constructGhostTunnelIpUrl()` signs `{ host, address, protocol, expiresAt }`.
|
|
442
|
+
- `resolveGhostTunnelIpRedirect()` rejects tampered, expired, wrong-host, or private-network claims unless private-network IPs are explicitly allowed.
|
|
443
|
+
- The redirect target port still comes from the exact `.ghosttunnel` entry, not from the shared URL.
|
|
444
|
+
|
|
445
|
+
For `transport: "tunnel"`, the handler uses the shared store instead of a target selector:
|
|
446
|
+
|
|
447
|
+
- The deployed handler resolves the exact host from `.ghosttunnel`, then checks for a live route heartbeat.
|
|
448
|
+
- The local agent only heartbeats exact hosts from `.ghosttunnel`.
|
|
449
|
+
- Request and response bodies are size-limited and encoded through the store.
|
|
450
|
+
- Hop-by-hop and `x-localghost-*` internal headers are stripped before forwarding.
|
|
451
|
+
- If the route is missing, offline, or times out, the deployed handler returns a safe status page instead of leaking target details.
|
|
452
|
+
|
|
267
453
|
Run the guardrail tests locally:
|
|
268
454
|
|
|
269
455
|
```sh
|
package/docs/github.md
CHANGED
|
@@ -4,11 +4,11 @@ Use this copy for the GitHub repo About box, topics, and social cards. Keep it s
|
|
|
4
4
|
|
|
5
5
|
## Repository Description
|
|
6
6
|
|
|
7
|
-
Friendly local hostnames for app repos. Install
|
|
7
|
+
Friendly local hostnames for app repos. Install with npm, pnpm, Yarn, or Bun and get clean `.localhost` URLs with Caddy and Vite-safe hosts.
|
|
8
8
|
|
|
9
9
|
Shorter alternative:
|
|
10
10
|
|
|
11
|
-
Friendly local hostnames for app repos.
|
|
11
|
+
Friendly local hostnames for app repos. Add the package, run `localghost`, ready.
|
|
12
12
|
|
|
13
13
|
## Topics
|
|
14
14
|
|
|
@@ -62,7 +62,7 @@ After creating `hamedb89/localghost`, this sets the public repo metadata:
|
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
64
|
gh repo edit hamedb89/localghost \
|
|
65
|
-
--description "Friendly local hostnames for app repos. Install
|
|
65
|
+
--description "Friendly local hostnames for app repos. Install with npm, pnpm, Yarn, or Bun and get clean .localhost URLs with Caddy and Vite-safe hosts." \
|
|
66
66
|
--homepage "https://hamedb89.github.io/localghost/" \
|
|
67
67
|
--add-topic localhost \
|
|
68
68
|
--add-topic local-development \
|
|
@@ -81,7 +81,7 @@ gh repo edit hamedb89/localghost \
|
|
|
81
81
|
The first visible paragraph should make the entrypoint feel obvious before it gets into configuration:
|
|
82
82
|
|
|
83
83
|
```txt
|
|
84
|
-
Localghost is a tiny Node.js CLI for friendly local domains in app repos. Add it
|
|
84
|
+
Localghost is a tiny Node.js CLI for friendly local domains in app repos. Add it with your package manager, run `localghost`, and use `http://app.localhost/` instead of remembering which localhost port belongs to which process.
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
Then the next docs layer can explain `.localghost`, Caddy, `/etc/hosts`, Vite `allowedHosts`, and configuration options.
|
package/docs/localghost.1.md
CHANGED
|
@@ -7,6 +7,7 @@ localghost - friendly local hostnames for app repos
|
|
|
7
7
|
## Synopsis
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
+
localghost [--cwd path] [--dry-run]
|
|
10
11
|
localghost init [--write-scripts] [--config file] [--host host] [--port port]
|
|
11
12
|
localghost doctor
|
|
12
13
|
localghost setup [--project name] [--config file] [--config-pattern regex] [--https|--ssl]
|
|
@@ -15,12 +16,26 @@ localghost reset [--project name]
|
|
|
15
16
|
localghost teardown [--project name] [--remove-caddyfile]
|
|
16
17
|
localghost status [--ready] [--json]
|
|
17
18
|
localghost ps [--json]
|
|
19
|
+
localghost tunnel [--cwd path] [--config file] [--config-pattern regex] [--ghost-config file] [--target-host host]
|
|
18
20
|
localghost update [--json]
|
|
19
|
-
localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--
|
|
20
|
-
localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--
|
|
21
|
+
localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust]
|
|
22
|
+
localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust] [--dynamic-port] -- command
|
|
21
23
|
localghost print [--config file] [--config-pattern regex]
|
|
22
24
|
```
|
|
23
25
|
|
|
26
|
+
With no subcommand, Localghost detects npm, pnpm, Yarn, or Bun from `packageManager` or a lockfile, prefers a non-recursive `dev:raw` script, falls back to `dev`, and runs the result through the normal Caddy lifecycle. `--dry-run` prints the detected command without checking or changing machine setup. Set `command: ["<package-manager>", "dev:web"]` in `localghost.config.mjs` when inference should be explicit.
|
|
27
|
+
|
|
28
|
+
For multiple independently started applications, configure `services` in `localghost.config.mjs`. Each service declares a unique name and host plus its project-relative working directory, requested port, and optional command. Bare `localghost` starts one Caddy process and all service commands, passes service-specific `LOCALGHOST_PORT`, `VITE_PORT`, and `LOCALGHOST_SERVICE` variables, and stops the group when any process exits.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
export default {
|
|
32
|
+
services: [
|
|
33
|
+
{ name: "web", cwd: "apps/web", host: "xyz.localhost", port: 5173 },
|
|
34
|
+
{ name: "api", cwd: "apps/api", host: "api.xyz.localhost", port: 8787 }
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
24
39
|
## Description
|
|
25
40
|
|
|
26
41
|
Localghost reads `.localghost`, optionally reads `localghost.config.mjs`, writes a managed `/etc/hosts` block, records `ops/local/localghost-state.json`, generates `ops/local/Caddyfile`, and runs a Caddy local proxy. The project name is derived from `package.json`, port `5173` is the fallback, HTTP is the default, dynamic ports are on by default, and local HTTPS is explicit with `--https`, `--ssl`, or `https: true` in `localghost.config.mjs`. It is intentionally small and explicit: no hidden installs, no full hosts-file rewrites, no surprise browser tabs, and no broad Vite `allowedHosts: true` shortcut.
|
|
@@ -48,7 +63,7 @@ Options:
|
|
|
48
63
|
- `--port <number>`: primary app port.
|
|
49
64
|
- `--api-host <host>`: API hostname.
|
|
50
65
|
- `--api-port <number>`: API port.
|
|
51
|
-
- `--package-manager <npm|yarn|
|
|
66
|
+
- `--package-manager <npm|pnpm|yarn|bun>`: package manager for suggested commands.
|
|
52
67
|
- `--write-scripts`: add Localghost scripts to `package.json`.
|
|
53
68
|
- `--force`: overwrite an existing config file.
|
|
54
69
|
|
|
@@ -78,6 +93,15 @@ Validates the HTTPS Caddyfile and runs `caddy trust --config <Caddyfile>` so bro
|
|
|
78
93
|
localghost trust
|
|
79
94
|
```
|
|
80
95
|
|
|
96
|
+
### repair
|
|
97
|
+
|
|
98
|
+
Reconciles the managed hosts block, regenerates and validates the Caddyfile, and refreshes project setup state. Use `--https --trust` to also re-run Caddy's local certificate trust step.
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
localghost repair
|
|
102
|
+
localghost repair --https --trust
|
|
103
|
+
```
|
|
104
|
+
|
|
81
105
|
### teardown
|
|
82
106
|
|
|
83
107
|
Removes the managed Localghost block from `/etc/hosts` for the selected project and records the action in `ops/local/localghost-state.json`. It leaves `ops/local/Caddyfile` in place unless `--remove-caddyfile` is passed.
|
|
@@ -112,6 +136,16 @@ localghost ps
|
|
|
112
136
|
localghost ps --json
|
|
113
137
|
```
|
|
114
138
|
|
|
139
|
+
### tunnel
|
|
140
|
+
|
|
141
|
+
Runs the local Ghost Tunnel agent for `ghostTunnel.transport: "tunnel"`. The command reads the Localghost project config, reads exact public hosts from `.ghosttunnel`, sends route heartbeats to the configured Redis REST store, polls for queued requests, and serves them from the matching local port.
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
localghost tunnel
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Use `--ghost-config <file>` when the exact route file is not `.ghosttunnel`. Use `--target-host <host>` when the local app listens somewhere other than `127.0.0.1`.
|
|
148
|
+
|
|
115
149
|
### update
|
|
116
150
|
|
|
117
151
|
Checks npm for a newer Localghost release. Pass `--json` for scripts and agents.
|
|
@@ -130,7 +164,7 @@ localghost routes
|
|
|
130
164
|
|
|
131
165
|
### dev
|
|
132
166
|
|
|
133
|
-
|
|
167
|
+
Performs a read-only readiness check, repairs stale setup when needed, writes and validates `ops/local/Caddyfile`, and runs Caddy. Supports `--config` and `--config-pattern`. HTTP is the default. Pass `--https` or `--ssl` to run a local HTTPS proxy, `--auto-repair=no` for strict failure behavior, or `--trust` to force the Caddy trust step before the proxy stays running.
|
|
134
168
|
|
|
135
169
|
```sh
|
|
136
170
|
localghost dev
|
|
@@ -146,13 +180,15 @@ localghost run --trust -- vite
|
|
|
146
180
|
localghost run --dynamic-port=no -- vite
|
|
147
181
|
```
|
|
148
182
|
|
|
149
|
-
By default, Localghost starts at the configured port and walks upward until `127.0.0.1:<port>` is free. Pass `--dynamic-port=no`
|
|
183
|
+
By default, Localghost starts at the configured port and walks upward until `127.0.0.1:<port>` is free. It performs a read-only readiness check and repairs stale setup before starting the child. Pass `--dynamic-port=no` for strict fixed-port behavior or `--auto-repair=no` to fail instead of repairing.
|
|
150
184
|
|
|
151
|
-
When `localghost.config.mjs` exists, `run`, `dev`, `setup`, `status`, `routes`, and the Vite plugin use it as an override layer. Most repos can skip it; add it only for decisions like `https: true`, `dynamicPort: false`, `wwwAlias: false`, custom ports, explicit project names, or the production `ghostTunnel` opt-in.
|
|
185
|
+
When `localghost.config.mjs` exists, `run`, `dev`, `setup`, `status`, `routes`, and the Vite plugin use it as an override layer. Most repos can skip it; add it only for decisions like `https: true`, `dynamicPort: false`, `autoRepair: false`, `wwwAlias: false`, custom ports, explicit project names, or the production `ghostTunnel` opt-in.
|
|
152
186
|
|
|
153
187
|
`ghostTunnel` does not change local Caddy or `/etc/hosts` setup. It marks `<route>-<project>-<owner>.ghost.<domain>` as a production app entrypoint. Use `ghostTunnel: { domains: "example.com", mode: "manual" }` when the production base domain is known, or omit `domains` to keep logs wildcarded as `https://<route>-<project>-<owner>.ghost.*/`. Production code can call `readLocalghostProjectConfig()`, `constructGhostTunnelUrl()`, and `assertSecureGhostTunnelRequest()` to read the flag, construct default tunnel URLs, validate the wildcard host shape, require HTTPS by default, and require an app-authenticated request by default.
|
|
154
188
|
|
|
155
|
-
|
|
189
|
+
`ghostTunnel.transport` is separate from the deployment adapter. `transport: "none"` proves ingress and exact-route lookup without forwarding. `transport: "ip"` signs a direct address into the shared URL, verifies that token against the exact ghost host, and redirects to the signed address plus the local port from `.ghosttunnel`. `transport: "tunnel"` uses Redis REST env vars plus `localghost tunnel` to queue deployed requests for a local agent. For LAN or private-address IP redirects, set `transport: { kind: "ip", allowPrivateNetworkAddress: true }` explicitly.
|
|
190
|
+
|
|
191
|
+
Relay helpers are private by default. Registration requires an authenticated local-agent bearer token plus an exact signed route claim. Targets must be explicit local host/port objects, dangerous ports are blocked, private/LAN targets require explicit opt-in, internal and hop-by-hop headers are stripped, sensitive logs are redacted, and offline agents get a safe 503 page. The IP transport follows the same posture: the shared URL carries only a signed `{ host, address, protocol, expiresAt }` claim, while the redirect port still comes from the exact `.ghosttunnel` entry.
|
|
156
192
|
|
|
157
193
|
When `ghostTunnel` is configured, route output and Vite startup logs print the production URL shape. Manual mode can fill local defaults for `route`, `project`, and `owner`; public mode leaves those slots as `<route>`, `<project>`, and `<owner>` unless `ghostTunnel.preview` pins a concrete URL. Add `ghostTunnel.domains` to fill one or more production base domains. When `ghostTunnel.preview` is configured with `route`, `project`, and `owner`, logs print the concrete URL, inheriting `ghostTunnel.domains` unless `preview.domain` is set. In an interactive Vite terminal, press `g` to show Ghost Tunnel configuration and open a numbered concrete URL.
|
|
158
194
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hamedb89/localghost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Friendly local hostnames for app repos with .localghost, Caddy, /etc/hosts, and Vite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"macos:widget:build": "bash apps/macos-widget/build.sh",
|
|
46
46
|
"dev": "tsx src/cli.ts",
|
|
47
47
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
48
|
+
"test:e2e": "npm run build && node --test tests/e2e/*.test.mjs",
|
|
48
49
|
"test:coverage": "npm run build && LOCALGHOST_TEST_SOURCE=1 node --import tsx --test --experimental-test-coverage --test-coverage-include=src/relay.ts --test-coverage-include=src/tunnel.ts --test-coverage-lines=90 --test-coverage-branches=75 --test-coverage-functions=100 tests/*.test.mjs",
|
|
49
50
|
"test:cli": "npm run build && node --test tests/cli.test.mjs",
|
|
50
51
|
"typecheck": "tsc --noEmit",
|