@hamedb89/localghost 0.1.9 → 0.1.12

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.
@@ -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.socialworkouts.app
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,18 +19,20 @@ import { defineLocalghostConfig } from "@hamedb89/localghost";
19
19
 
20
20
  export default defineLocalghostConfig({
21
21
  ghostTunnel: {
22
- domains: "socialworkouts.app",
23
- mode: "manual"
22
+ domains: "moonlit-otter.example",
23
+ mode: "manual",
24
+ adapter: "vercel",
25
+ transport: "none"
24
26
  }
25
27
  });
26
28
  ```
27
29
 
28
- With `ghostTunnel: { domains }`, route output and Vite startup output use local defaults for `route`, `project`, and `owner`, then fill the configured domain:
30
+ With `ghostTunnel: { domains }` in the default manual mode, route output and Vite startup output use local defaults for `route`, `project`, and `owner`, then fill the configured domain:
29
31
 
30
32
  ```txt
31
33
  localghost ghost tunnel
32
34
  mode: manual
33
- expected: https://app-decision-layer-hamed.ghost.socialworkouts.app/
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 `*`:
@@ -43,12 +45,65 @@ localghost ghost tunnel
43
45
 
44
46
  `manual` is the default activation mode. `ghostTunnel: "manual"` and `ghostTunnel: "public"` are shorthand modes. Use `enabled: false` to keep domains/config in the file without exposing the tunnel surface.
45
47
 
48
+ Public mode keeps the namespace flexible unless you configure a preview. This is useful for deployed wildcard endpoints that react to whichever route arrives:
49
+
50
+ ```txt
51
+ localghost ghost tunnel
52
+ mode: public
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
+ });
99
+ ```
100
+
46
101
  Use object form to override defaults or provide a concrete preview URL:
47
102
 
48
103
  ```js
49
104
  export default defineLocalghostConfig({
50
105
  ghostTunnel: {
51
- domains: "socialworkouts.app",
106
+ domains: "moonlit-otter.example",
52
107
  preview: {
53
108
  route: "plan",
54
109
  project: "summer-base",
@@ -63,9 +118,170 @@ export default defineLocalghostConfig({
63
118
  1. Add `ghostTunnel: { domains: "your-domain.com" }`, `ghostTunnel: "manual"`, or `ghostTunnel.preview` to `localghost.config.mjs`.
64
119
  2. Point the wildcard DNS record for `*.ghost.<your-domain>` at the deployed app.
65
120
  3. Route `*.ghost.<your-domain>` to the same production app that serves the Vite build.
66
- 4. In production request handling, read the Localghost project config without resolving local `.localghost` setup.
67
- 5. Construct tunnel URLs from `route`, `project`, and `owner`.
68
- 6. Validate the incoming request host, protocol, and auth before serving the tunnel surface.
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.
249
+
250
+ ## Vercel DNS
251
+
252
+ For Vercel, add the Ghost Tunnel wildcard domain to the project first:
253
+
254
+ ```txt
255
+ *.ghost.copper-comet.example
256
+ ```
257
+
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:
259
+
260
+ ```txt
261
+ Name: *.ghost
262
+ Type: ALIAS
263
+ Value: <the Vercel DNS target shown for the project>
264
+ TTL: 60
265
+ ```
266
+
267
+ For example, if Vercel shows `cname.vercel-dns-016.com` as the project target, use the fully-qualified value with the trailing dot when the DNS form requires it:
268
+
269
+ ```txt
270
+ Name: *.ghost
271
+ Type: ALIAS
272
+ Value: cname.vercel-dns-016.com.
273
+ TTL: 60
274
+ ```
275
+
276
+ When DNS is managed somewhere else, use the provider's wildcard subdomain shape:
277
+
278
+ ```txt
279
+ Name: *.ghost
280
+ Type: CNAME
281
+ Value: cname.vercel-dns-016.com.
282
+ ```
283
+
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`.
69
285
 
70
286
  ```ts
71
287
  import {
@@ -77,7 +293,7 @@ import {
77
293
  const { config } = await readLocalghostProjectConfig();
78
294
 
79
295
  const url = constructGhostTunnelUrl({
80
- domain: "socialworkouts.app",
296
+ domain: "moonlit-otter.example",
81
297
  route: "plan",
82
298
  project: "summer-base",
83
299
  owner: "hamed",
@@ -86,13 +302,13 @@ const url = constructGhostTunnelUrl({
86
302
 
87
303
  const route = assertSecureGhostTunnelRequest({
88
304
  host: request.headers.get("host") ?? "",
89
- domain: "socialworkouts.app",
305
+ domain: "moonlit-otter.example",
90
306
  protocol: request.url.startsWith("https:") ? "https" : "http",
91
307
  authenticated: Boolean(session),
92
308
  ghostTunnel: config.ghostTunnel
93
309
  });
94
310
 
95
- // url is https://plan-summer-base-hamed.ghost.socialworkouts.app/
311
+ // url is https://plan-summer-base-hamed.ghost.moonlit-otter.example/
96
312
  // route.namespace is { route: "plan", project: "summer-base", owner: "hamed" }.
97
313
  ```
98
314
 
@@ -101,7 +317,7 @@ When `ghostTunnel.preview` is configured, Localghost logs the concrete preview U
101
317
  ```txt
102
318
  localghost ghost tunnel
103
319
  mode: manual
104
- expected: https://plan-summer-base-hamed.ghost.socialworkouts.app/
320
+ expected: https://plan-summer-base-hamed.ghost.moonlit-otter.example/
105
321
  ```
106
322
 
107
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.
@@ -115,7 +331,7 @@ The default namespace tags are `route`, `project`, and `owner`, joined with `-`.
115
331
  ```js
116
332
  export default defineLocalghostConfig({
117
333
  ghostTunnel: {
118
- domains: "socialworkouts.app"
334
+ domains: "moonlit-otter.example"
119
335
  }
120
336
  });
121
337
  ```
@@ -150,7 +366,7 @@ For custom tags, pass extra values to the constructor:
150
366
 
151
367
  ```ts
152
368
  constructGhostTunnelUrl({
153
- domain: "socialworkouts.app",
369
+ domain: "moonlit-otter.example",
154
370
  route: "plan",
155
371
  project: "summer-base",
156
372
  owner: "hamed",
@@ -165,7 +381,7 @@ constructGhostTunnelUrl({
165
381
 
166
382
  - `ghostTunnel` is opt-in and resolves to disabled unless the project config enables it.
167
383
  - The default production entry host is `ghost.<your-domain>`, with a wildcard of `*.ghost.<your-domain>`.
168
- - The default wildcard label must be `route-project-owner`, such as `plan-summer-base-hamed.ghost.socialworkouts.app`.
384
+ - The default wildcard label must be `route-project-owner`, such as `plan-summer-base-hamed.ghost.moonlit-otter.example`.
169
385
  - The configured spread tag may contain the namespace separator. By default, that is `project`.
170
386
  - Other namespace values cannot include the namespace separator, because parsing must be reversible.
171
387
  - Host labels must be DNS-safe lowercase ASCII labels after normalization.
@@ -175,7 +391,7 @@ constructGhostTunnelUrl({
175
391
 
176
392
  ## Relay Security
177
393
 
178
- Localghost relay is private by default. Public requests can select a Ghost Tunnel route, but they must never select the local target URL, hostname, IP, or port. There must be no `/proxy?url=...` style endpoint.
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.
179
395
 
180
396
  Route registration goes through an authenticated local agent:
181
397
 
@@ -186,7 +402,7 @@ import {
186
402
  } from "@hamedb89/localghost";
187
403
 
188
404
  const claim = signRelayRouteClaim({
189
- host: "plan-summer-base-hamed.ghost.socialworkouts.app",
405
+ host: "plan-summer-base-hamed.ghost.moonlit-otter.example",
190
406
  scope: "socialworkouts:preview",
191
407
  agentId: "local-agent-1",
192
408
  expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString()
@@ -220,6 +436,20 @@ The relay helpers enforce these rules:
220
436
  - `renderRelayOfflineResponse()` returns a safe offline page with no secrets or stack traces.
221
437
  - Vite integration continues to generate explicit `allowedHosts`; it never sets `allowedHosts: true`.
222
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
+
223
453
  Run the guardrail tests locally:
224
454
 
225
455
  ```sh
@@ -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] [--setup] [--trust]
20
- localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--setup] [--trust] [--dynamic-port] -- command
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 the package manager 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: ["pnpm", "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, command: ["pnpm", "dev"] },
34
+ { name: "api", cwd: "apps/api", host: "api.xyz.localhost", port: 8787, command: ["pnpm", "dev"] }
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.
@@ -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
- Requires setup to be ready, writes `ops/local/Caddyfile`, validates it, and runs Caddy. Supports `--config` and `--config-pattern`. HTTP is the default. Pass `--https` or `--ssl` to run a local HTTPS proxy. Pass `--setup` to explicitly allow `dev` to run setup first when setup is missing or stale. Pass `--trust` to force the Caddy trust step before the proxy stays running.
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,15 +180,17 @@ 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` when you want strict fixed-port behavior. Pass `--setup` to explicitly allow setup when the hosts block is missing or stale.
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
- 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.
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
- When `ghostTunnel` is configured, route output and Vite startup logs print local defaults for `route`, `project`, and `owner`. Add `ghostTunnel.domains` to fill one or more production base domains. When `ghostTunnel.preview` is configured with `route`, `project`, and `owner`, they 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.
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
 
159
195
  `dev` and `run` register active sessions in a user-local activity file so `localghost ps` can show what is running across projects.
160
196
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamedb89/localghost",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
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",