nylorun 0.6.0-beta → 0.7.0-beta

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # nylorun
2
2
 
3
+ ## 0.7.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 9af30d8: **The Studio proxy is removed, and Restate runs with its own memory defaults.** Each Tenant's Studio is at `http://localhost:<port>` again (`nylorun studio --tenant <name>` opens it signed in): its own session cookie already keeps it apart from other Studios, so the proxy only added a container and a second address. The 256 MiB RocksDB cap did not lower Restate's memory, so it is gone. The first command of this release removes the proxy that 0.6 started (`nylorun-proxy` and `~/.nylorun/proxy/`); `NYLORUN_PROXY_PORT`, `NYLORUN_PROXY_DISABLED`, `studio.proxyUrl` and Studio's `NYLORUN_STUDIO_PUBLIC_ORIGINS` are gone.
8
+
9
+ ### Patch Changes
10
+
11
+ - Pin studio to the tested release.
12
+
3
13
  ## 0.6.0-beta
4
14
 
5
15
  ### Minor Changes
package/README.md CHANGED
@@ -36,7 +36,7 @@ npx nylorun doctor # Node 24+, Docker, Compose v2, and the Tenan
36
36
  ```sh
37
37
  nylorun up|start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]
38
38
  # create (first run) and start the Tenant; link the project; print the URLs; open Studio signed in
39
- nylorun down|stop [--tenant <name> | --all] # stop the containers (--all: every Tenant's, and the Studio proxy); keep volumes
39
+ nylorun down|stop [--tenant <name> | --all] # stop the containers (--all: every Tenant's); keep volumes
40
40
  nylorun status [--tenant <name>] [--json] # the Tenant, its Host root and id, services, endpoints, Runtime health
41
41
  nylorun logs [service] [--tenant <name>] [-f] [--tail <n>] # postgres, restate, s2-lite, gateway, runtime, studio
42
42
  nylorun studio [--tenant <name>] [--no-open] # sign a browser in to Studio on the Tenant; starts it if needed
@@ -82,29 +82,29 @@ projects and volumes.
82
82
 
83
83
  Tenants run side by side, each on its own ports, containers, network and
84
84
  volumes ([The containers](#the-containers)); a running Tenant uses about
85
- 600–700 MB. Each Studio has its own address through the
86
- [Studio proxy](#the-studio-proxy), `http://<name>.localhost:4160`. `nylorun ls`
85
+ 1.2 GB. Each Studio is on its own port, `http://localhost:<port>`, with its own
86
+ session cookie, so two Studios in one browser stay signed in;
87
+ `nylorun studio --tenant <name>` opens any Tenant's Studio signed in. `nylorun ls`
87
88
  lists them with their state, the memory their containers use (`MEMORY`: the sum
88
89
  of `docker stats` over the containers labelled `dev.nylorun.tenant=<name>`, `-`
89
90
  when stopped; `memoryBytes` in `--json`, `null` when stopped or unknown), the
90
91
  Runtime's URL and Studio's:
91
92
 
92
93
  ```text
93
- TENANT STATE MEMORY RUNTIME STUDIO PROJECT
94
- api running 652 MB http://localhost:8790 http://api.localhost:4160 /Users/me/api
95
- default stopped - http://localhost:8787 http://default.localhost:4160 -
94
+ TENANT STATE MEMORY RUNTIME STUDIO PROJECT
95
+ api running 1.2 GB http://localhost:8790 http://localhost:4162 /Users/me/api
96
+ default stopped - http://localhost:8787 http://localhost:4161 -
96
97
  ```
97
98
 
98
99
  When other Tenants are running, `start` says so after its summary, on stderr
99
100
  (without the size when Docker does not report it):
100
101
 
101
102
  ```text
102
- Also running: default, api (about 1.3 GB). "nylorun stop --all" stops them all.
103
+ Also running: default, api (about 2.4 GB). "nylorun stop --all" stops them all.
103
104
  ```
104
105
 
105
- `nylorun stop --all` stops every running Tenant on this machine and the Studio
106
- proxy, keeping their volumes; it cannot be combined with `--tenant` (exit 2).
107
- `stop` of one Tenant leaves the proxy running.
106
+ `nylorun stop --all` stops every running Tenant on this machine, keeping their
107
+ volumes; it cannot be combined with `--tenant` (exit 2).
108
108
 
109
109
  A Tenant created by nylorun 0.5 keeps its data in volumes Compose named
110
110
  `<project>_postgres`, `<project>_restate`, `<project>_s2` and
@@ -167,67 +167,31 @@ project, and each has the label `dev.nylorun.tenant: <name>`. For Tenant `shop`:
167
167
  | Network | `nylorun-shop` |
168
168
  | Volumes | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-workspaces` |
169
169
 
170
- Postgres initialises the Tenant's database with C collation. Restate's RocksDB
171
- memory is capped at 256 MiB (`RESTATE_ROCKSDB_TOTAL_MEMORY_SIZE`; Restate's
172
- default is 2 GiB), so several Tenants fit on a laptop: a Tenant uses about
173
- 600–700 MB, down from about 1.3 GB. The Runtime and Studio images are pinned by
170
+ Postgres initialises the Tenant's database with C collation. Restate runs with
171
+ its own defaults. A running Tenant uses about 1.2 GB, most of it Restate; stop
172
+ the Tenants you are not using (`nylorun stop`, or `nylorun stop --all`). The Runtime and Studio images are pinned by
174
173
  this release (`package.json` `nylorun.runtime` and `nylorun.studio`);
175
174
  `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them (local builds,
176
175
  CI). Ports publish on loopback only: the Runtime on `8787`, its operator
177
176
  listener (Admin API) on `8788`, Studio on `4161` and the Restate UI on `9070`,
178
177
  or free ports chosen on the Tenant's first start, avoiding the ports other
179
- Tenants and the Studio proxy keep, and kept in its `.env`.
178
+ Tenants keep, and kept in its `.env`.
180
179
 
181
- `start`, `ls` and `nylorun studio` give Studio as `http://<name>.localhost:4160`,
182
- its address through the Studio proxy (below); `status` adds Studio's own
183
- `http://localhost:<port>` (`--json` has both: `studio.url` is Studio's own port,
184
- which an embedding app frames, and `studio.proxyUrl` the proxy's). Studio has no
180
+ `start`, `status`, `ls` and `nylorun studio` give Studio as
181
+ `http://localhost:<port>` (`studio.url` in `status --json`). Studio has no
185
182
  password: in a terminal (not in CI, and not with `--no-open`), `start` asks the
186
183
  Studio container for a single-use login token with the admin key (`POST
187
- /_studio/login-tokens`, through the proxy; on Studio's own port, saying so,
188
- when the proxy does not answer) and opens `…/login?token=…` on the Tenant's
189
- page in the browser. That sets a session cookie for 30 days, which survives
190
- Studio restarts, so the printed URL keeps working in that browser. The token
191
- itself is never printed unless no browser starts. Otherwise `start` says to run
192
- `nylorun studio`, which signs a browser in the same way; `nylorun studio
184
+ /_studio/login-tokens`) and opens `http://localhost:<port>/login?token=…` on the
185
+ Tenant's page in the browser. That sets a session cookie for 30 days, which
186
+ survives Studio restarts, so the printed URL keeps working in that browser. The
187
+ token itself is never printed unless no browser starts. Otherwise `start` says
188
+ to run `nylorun studio`, which signs a browser in the same way; `nylorun studio
193
189
  --no-open` prints the login URL (it works once, for two minutes) instead.
194
190
 
195
- Each Tenant's Studio sets its own session cookie, `nylorun_studio_<name>`
196
- (`NYLORUN_STUDIO_SESSION_COOKIE`).
197
-
198
- ### The Studio proxy
199
-
200
- One small Caddy container per machine (`nylorun-proxy`, Compose project
201
- `nylorun-proxy`, label `dev.nylorun.proxy: "true"`) gives every Tenant's Studio
202
- the address `http://<name>.localhost:<port>`. Browsers keep cookies per host,
203
- not per port, so separate hosts keep the Studios' sessions apart. The proxy is
204
- for browsers only: programs keep using the Runtime's `http://localhost:<port>`.
205
- It holds no Tenant data; its files are in `~/.nylorun/proxy/` (`compose.yaml`,
206
- `Caddyfile`, and `.env` with `NYLORUN_PROXY_PORT`, 4160 or a free port chosen
207
- on its first start and kept; Caddy's own state is tmpfs). Its image is pinned
208
- by this release (`caddy:2.11.6`).
209
-
210
- - Caddy listens on port 80 in the container, published at `NYLORUN_PROXY_PORT`
211
- on `127.0.0.1` and `[::1]` (macOS resolves `*.localhost` to `::1` only); when
212
- Docker refuses `::1` (IPv6 off), it publishes on `127.0.0.1` alone and says
213
- so once.
214
- - It routes every Tenant under `~/.nylorun/tenants/`: it joins each Tenant's
215
- network and reaches `<project>-studio:3000`, passing the browser's `Host`
216
- through, which Studio checks. A stopped Tenant answers 502 naming
217
- `nylorun start --tenant <name>`; an unknown host 404 naming `nylorun ls`.
218
- - `start` rewrites its files, brings it up (only when Studio starts: not with
219
- `--no-studio`), joins every Tenant's network and reloads Caddy; Studio learns
220
- its proxy origin from `NYLORUN_STUDIO_PUBLIC_ORIGINS`, written to the
221
- Tenant's `.env` on every start. A proxy that does not come up never fails
222
- `start`: it warns once and prints Studio's own URL.
223
- - `reset` and `delete` detach it from the Tenant's network first; `delete`
224
- removes the Tenant's route. `stop --all` stops it.
225
- - `NYLORUN_PROXY_DISABLED=1` turns it off: Studio is `http://localhost:<port>`.
226
- A Tenant under `NYLORUN_HOME` or `NYLORUN_COMPOSE_PROJECT` (the repository's
227
- smokes) does not use it either.
228
-
229
- `nylorun doctor` reports it on a `proxy` row: running (on which loopbacks and
230
- port), stopped, not created yet, or disabled.
191
+ Browsers share cookies across the ports of one host, so each Tenant's Studio
192
+ sets its own session cookie, `nylorun_studio_<name>`
193
+ (`NYLORUN_STUDIO_SESSION_COOKIE`): signing in to one Studio does not sign you
194
+ out of another.
231
195
 
232
196
  ### Embedding Studio in a desktop app
233
197
 
@@ -308,7 +272,7 @@ containers, volumes and Host root: the vault key (KEK) and all its data go with
308
272
  | Port conflict | Change `NYLORUN_PORT` / `NYLORUN_STUDIO_PORT` in `~/.nylorun/tenants/<name>/docker/.env` |
309
273
  | Logs | `nylorun logs runtime -f` |
310
274
  | Studio login expired | `nylorun studio` |
311
- | `http://<name>.localhost:<port>` does not answer | `nylorun doctor` (the `proxy` row); `nylorun start` brings the proxy up; Studio's own port is in `nylorun status` |
275
+ | Which Studio is which | `nylorun ls` lists each Tenant's Studio URL; `nylorun studio --tenant <name>` opens one signed in |
312
276
  | `Tenant … was created by nylorun 0.5` (exit 3) | `nylorun reset --tenant <name>` (its data starts fresh) |
313
277
 
314
278
  See [MIGRATION.md](../MIGRATION.md).
package/dist/doctor.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Check, type ProxyStatus, type StackDeps, type StackStatus } from "./stack/index.js";
1
+ import { type Check, type StackDeps, type StackStatus } from "./stack/index.js";
2
2
  export interface StackDoctorReport {
3
3
  node: {
4
4
  version: string;
@@ -9,8 +9,6 @@ export interface StackDoctorReport {
9
9
  tenant?: Pick<StackStatus, "name" | "project" | "home" | "state" | "runtime" | "tenant" | "studio" | "gateway">;
10
10
  /** Why no Tenant was checked: none is selected here (a project without a link). */
11
11
  noTenant?: string;
12
- /** The machine's Studio proxy (`http://<tenant>.localhost:<port>`); never a failure. */
13
- proxy?: ProxyStatus;
14
12
  }
15
13
  /**
16
14
  * `nylorun doctor`: check the prerequisites of a local Tenant (Node 24+,
package/dist/doctor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "./errors.js";
2
- import { checkDocker, defaultStackDeps, readProxyReport, readStackStatus, } from "./stack/index.js";
2
+ import { checkDocker, defaultStackDeps, readStackStatus, } from "./stack/index.js";
3
3
  /** Oldest Node nylorun and the developer's application run on. */
4
4
  const MIN_NODE_MAJOR = 24;
5
5
  /**
@@ -36,7 +36,6 @@ export async function doctorStack(options) {
36
36
  throw error;
37
37
  report.noTenant = error.message;
38
38
  }
39
- report.proxy = await readProxyReport(deps);
40
39
  }
41
40
  const broken = report.tenant?.state === "running" &&
42
41
  (!report.tenant.runtime.healthy ||
@@ -96,25 +95,9 @@ export async function doctorStack(options) {
96
95
  ? `✓ ${local.tenant.id ?? "?"} open`
97
96
  : `✗ ${local.tenant.id ?? "?"} unavailable${local.tenant.cause ? `: ${local.tenant.cause.code}: ${local.tenant.cause.repair}` : " (opening)"}`,
98
97
  ]);
99
- rows.push(["studio", `${local.studio.proxyUrl ?? local.studio.url ?? "?"} · ${local.studio.state}`]);
98
+ rows.push(["studio", `${local.studio.url ?? "?"} · ${local.studio.state}`]);
100
99
  }
101
100
  }
102
- const proxy = report.proxy;
103
- if (proxy) {
104
- const on = proxy.port
105
- ? `127.0.0.1:${proxy.port}${proxy.ipv6 ? ` and [::1]:${proxy.port}` : ""}`
106
- : "";
107
- rows.push([
108
- "proxy",
109
- proxy.state === "running"
110
- ? `✓ running on ${on} (http://<tenant>.localhost:${proxy.port ?? "?"})`
111
- : proxy.state === "disabled"
112
- ? "- disabled (NYLORUN_PROXY_DISABLED): Studio is on each Tenant's own port"
113
- : proxy.state === "stopped"
114
- ? `- stopped${on ? ` (${on})` : ""}: nylorun start starts it`
115
- : "- not created yet: nylorun start creates it",
116
- ]);
117
- }
118
101
  const width = Math.max(...rows.map(([key]) => key.length)) + 2;
119
102
  for (const [key, value] of rows)
120
103
  log(` ${key.padEnd(width)}${value}`);
@@ -1,7 +1,6 @@
1
1
  import type { HostTenant } from "@nylorun/core/contracts";
2
2
  import { type ComposeService, type DockerRunner } from "./docker.js";
3
3
  import type { PortProbe } from "./ports.js";
4
- import { type ProxyStatus } from "./proxy.js";
5
4
  import { type FetchLike } from "./studio-login.js";
6
5
  export declare const STACK_SERVICES: readonly ["postgres", "restate", "s2-lite", "gateway", "runtime", "studio"];
7
6
  export declare const stackUsage: string;
@@ -74,10 +73,8 @@ export interface StackStatus {
74
73
  /** The Tenant as the Runtime reports it, while it answers. */
75
74
  tenant?: StackTenant;
76
75
  studio: {
77
- /** Studio's own port: what an embedding app frames (same site as its own localhost). */
76
+ /** `http://localhost:<port>`: also what an embedding app frames (same site as its own localhost). */
78
77
  url?: string;
79
- /** Through the Studio proxy, when it serves the Tenant. */
80
- proxyUrl?: string;
81
78
  state: string;
82
79
  /** Exact origins that may show Studio in a frame (Studio §8.9). */
83
80
  embedOrigins?: string[];
@@ -92,8 +89,6 @@ export interface StackStatus {
92
89
  };
93
90
  services: ComposeService[];
94
91
  }
95
- /** The Studio proxy, as data (`nylorun doctor`). */
96
- export declare function readProxyReport(deps: StackDeps): Promise<ProxyStatus>;
97
92
  /** What `nylorun status` reports, as data (`nylorun doctor`). */
98
93
  export declare function readStackStatus(deps: StackDeps, options?: {
99
94
  name?: string;
@@ -111,7 +106,7 @@ export interface StackEndpoints {
111
106
  hostId: string;
112
107
  adminKey: string;
113
108
  studioPort: number;
114
- /** Through the proxy when it serves the Tenant, else Studio's own port. */
109
+ /** `http://localhost:<studio port>` */
115
110
  studioUrl: string;
116
111
  /** Studio is running and healthy. */
117
112
  studioUp: boolean;
@@ -135,7 +130,7 @@ export declare function tenantStudioPath(tenantId: string): string;
135
130
  * Mint a fresh Studio login URL on a running Tenant, landing on `next`.
136
131
  * Undefined (after a warning) when Studio does not answer.
137
132
  */
138
- export declare function studioLoginUrl(deps: StackDeps, running: Pick<StackEndpoints, "studioPort" | "studioUrl" | "adminKey">, next?: string): Promise<string | undefined>;
133
+ export declare function studioLoginUrl(deps: StackDeps, running: Pick<StackEndpoints, "studioPort" | "adminKey">, next?: string): Promise<string | undefined>;
139
134
  /** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
140
135
  export declare function runStudioCommand(args: readonly string[], deps: StackDeps, options?: {
141
136
  next?: string;
@@ -13,8 +13,7 @@ import { readAdminKey, readHostConfig, STACK_CLIENT_HOST } from "./host-files.js
13
13
  import { runtimeImageOverridden, stackImages } from "./images.js";
14
14
  import { stackPaths } from "./paths.js";
15
15
  import { prepareStack, readStackEnv } from "./prepare.js";
16
- import { disconnectProxy, ensureProxySettings, PROXY_PROJECT, proxyDisabled, proxyOrigin, readProxySettings, readProxyStatus, refreshProxyRoutes, startProxy, stopProxy, } from "./proxy.js";
17
- import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
16
+ import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, removeOldProxy, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
18
17
  import { mintStudioLogin, studioOrigin } from "./studio-login.js";
19
18
  export const STACK_SERVICES = ["postgres", "restate", "s2-lite", "gateway", "runtime", "studio"];
20
19
  const CORE_SERVICES = ["postgres", "restate", "s2-lite", "gateway", "runtime"];
@@ -23,7 +22,7 @@ export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio
23
22
  credentials.json) on the first run; print the Runtime and Studio URLs and open Studio
24
23
  signed in (in a terminal). --tenant attaches to (or creates) a named Tenant; --no-link
25
24
  starts the default Tenant (or --tenant's) without linking the current directory
26
- down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's, and the Studio proxy); keep volumes
25
+ down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's); keep volumes
27
26
  status [--tenant <name>] [--json] the Tenant, its id, services, endpoints and Runtime health
28
27
  logs [service] [--tenant <name>] [-f] [--tail <n>]
29
28
  container logs (${STACK_SERVICES.join(", ")})
@@ -43,26 +42,11 @@ export function stackProject(env, name) {
43
42
  function nylorunRoot(deps) {
44
43
  return deps.nylorunRoot ?? defaultNylorunRoot();
45
44
  }
46
- /**
47
- * The Studio proxy serves the machine's Tenants (`~/.nylorun/tenants`), unless
48
- * NYLORUN_PROXY_DISABLED=1; a Host root or Compose project of its own (NYLORUN_HOME,
49
- * NYLORUN_COMPOSE_PROJECT, as the repository's smokes use) is outside it.
50
- */
51
- function proxied(ctx) {
52
- const { env } = ctx.deps;
53
- return !proxyDisabled(env) && !env.NYLORUN_HOME?.trim() && !env.NYLORUN_COMPOSE_PROJECT?.trim();
54
- }
55
- /** Studio's URL for browsers: through the proxy when it serves the Tenant, else its own port. */
56
- async function studioUrl(ctx, studioPort) {
57
- const settings = proxied(ctx) ? await readProxySettings(nylorunRoot(ctx.deps)) : undefined;
58
- return settings ? proxyOrigin(ctx.name, settings.port) : studioOrigin(studioPort);
59
- }
60
- /** The proxy's routes: every Tenant on this machine. */
61
- async function proxyRoutes(deps) {
62
- return (await listTenants(nylorunRoot(deps))).map((tenant) => ({
63
- name: tenant.name,
64
- project: stackProject(deps.env, tenant.name),
65
- }));
45
+ /** Before every Tenant command: move 0.4's Host roots, remove 0.6's Studio proxy. */
46
+ async function tidyMachine(deps) {
47
+ const base = nylorunRoot(deps);
48
+ await moveStackRoots(base, deps.err);
49
+ await removeOldProxy(deps.docker, base, deps.err);
66
50
  }
67
51
  /**
68
52
  * Compose's names for a 0.5 Tenant's volumes (`<project>_postgres`, …) that exist on this
@@ -78,13 +62,9 @@ async function oldVolumes(docker, project) {
78
62
  current: names.has(`${project}-postgres`),
79
63
  };
80
64
  }
81
- /**
82
- * Before `docker compose down` (`reset`, `delete`): detach the proxy from the Tenant's network
83
- * so Compose can remove it. After it: drop a 0.5 Tenant's volumes and network.
84
- */
65
+ /** `docker compose down` (`reset`, `delete`), then drop a 0.5 Tenant's volumes and network. */
85
66
  async function composeDown(ctx, args) {
86
67
  const { docker } = ctx.deps;
87
- await disconnectProxy(docker, ctx.project);
88
68
  const code = await docker.stream(args);
89
69
  if (code !== 0)
90
70
  throw new CliError(`docker compose down failed (exit ${code}).`, 1);
@@ -356,14 +336,9 @@ async function tenantPorts(base, except) {
356
336
  }
357
337
  return reserved;
358
338
  }
359
- /** Ports other Tenants and the proxy keep, so a new Tenant avoids them. */
339
+ /** Ports other Tenants keep, so a new Tenant avoids them. */
360
340
  async function reservedPorts(ctx) {
361
- const base = nylorunRoot(ctx.deps);
362
- const reserved = await tenantPorts(base, ctx.paths.root);
363
- const proxy = await readProxySettings(base);
364
- if (proxy)
365
- reserved.add(proxy.port);
366
- return reserved;
341
+ return await tenantPorts(nylorunRoot(ctx.deps), ctx.paths.root);
367
342
  }
368
343
  async function bringUp(ctx, options) {
369
344
  const { deps } = ctx;
@@ -377,10 +352,6 @@ async function bringUp(ctx, options) {
377
352
  const base = nylorunRoot(deps);
378
353
  const telemetry = await readTelemetry(base);
379
354
  const analytics = telemetryDecision(deps.env, telemetry).enabled;
380
- // The proxy's port avoids every Tenant's; a new Tenant's ports then avoid the proxy's.
381
- const proxy = proxied(ctx)
382
- ? await ensureProxySettings(base, deps.ports, await tenantPorts(base))
383
- : undefined;
384
355
  const prepared = await prepareStack({
385
356
  paths: ctx.paths,
386
357
  name: ctx.name,
@@ -400,7 +371,6 @@ async function bringUp(ctx, options) {
400
371
  : {}),
401
372
  ...(options.studioEmbedOrigins ? { studioEmbedOrigins: options.studioEmbedOrigins } : {}),
402
373
  ...(analytics ? { studioAnalyticsId: STUDIO_ANALYTICS_ID } : {}),
403
- ...(proxy ? { studioPublicOrigin: proxyOrigin(ctx.name, proxy.port) } : {}),
404
374
  });
405
375
  if (analytics && options.studio && telemetry.noticeShown === undefined) {
406
376
  deps.err(TELEMETRY_NOTICE);
@@ -430,45 +400,21 @@ async function bringUp(ctx, options) {
430
400
  if (!studioStarted)
431
401
  deps.err(`Warning: Studio did not start (image ${prepared.env.studioImage}). The Runtime is up; see "nylorun logs studio".`);
432
402
  }
433
- let studioUrl = studioOrigin(prepared.env.studioPort);
434
- if (studioStarted && proxy) {
435
- const up = await startProxy({
436
- docker: deps.docker,
437
- base,
438
- settings: proxy,
439
- routes: await proxyRoutes(deps),
440
- err: deps.err,
441
- ...(deps.pollMs !== undefined ? { pollMs: deps.pollMs } : {}),
442
- });
443
- if (up)
444
- studioUrl = proxyOrigin(ctx.name, proxy.port);
445
- }
446
403
  return {
447
404
  runtimeUrl,
448
405
  adminUrl,
449
406
  hostId: prepared.host.hostId,
450
407
  studioPort: prepared.env.studioPort,
451
- studioUrl,
408
+ studioUrl: studioOrigin(prepared.env.studioPort),
452
409
  studioStarted,
453
410
  adminKey: prepared.adminKey,
454
411
  };
455
412
  }
456
413
  const errorText = (error) => (error instanceof Error ? error.message : String(error));
457
- /**
458
- * A login URL on Studio's URL: through the proxy when it serves the Tenant (once; a proxy that
459
- * does not answer falls back, saying so), else on Studio's own port, retried while it starts.
460
- */
461
- async function tryStudioLogin(ctx, studio, adminKey) {
414
+ /** A login URL on Studio's own port, retried while it starts. */
415
+ async function tryStudioLogin(ctx, studioPort, adminKey) {
462
416
  const { deps } = ctx;
463
- const origin = studioOrigin(studio.port);
464
- if (studio.url !== origin) {
465
- try {
466
- return await mintStudioLogin({ fetch: deps.fetch, origin: studio.url, adminKey });
467
- }
468
- catch (error) {
469
- deps.err(`Studio did not answer through the proxy at ${studio.url} (${errorText(error)}); signing in at ${origin} instead.`);
470
- }
471
- }
417
+ const origin = studioOrigin(studioPort);
472
418
  const deadline = Date.now() + (deps.loginTimeoutMs ?? 15_000);
473
419
  let lastError = "";
474
420
  for (;;) {
@@ -584,7 +530,7 @@ async function start(deps, args) {
584
530
  if (started.studioStarted) {
585
531
  deps.out(`Studio ${started.studioUrl}`);
586
532
  if (opensBrowser(deps, flags)) {
587
- const login = await tryStudioLogin(ctx, { port: started.studioPort, url: started.studioUrl }, started.adminKey);
533
+ const login = await tryStudioLogin(ctx, started.studioPort, started.adminKey);
588
534
  if (login)
589
535
  await openLogin(ctx, withNext(login, tenantStudioPath(tenant.id)));
590
536
  }
@@ -631,7 +577,7 @@ async function stop(deps, args) {
631
577
  deps.out(`Stopped Tenant ${ctx.name}; its data is kept.`);
632
578
  return 0;
633
579
  }
634
- /** `stop --all`: every running Tenant on this machine, then the Studio proxy; volumes are kept. */
580
+ /** `stop --all`: every running Tenant on this machine; volumes are kept. */
635
581
  async function stopAll(deps) {
636
582
  await dockerPreflight(deps.docker);
637
583
  const projects = (await composeProjects(deps.docker)) ?? new Map();
@@ -648,17 +594,11 @@ async function stopAll(deps) {
648
594
  throw new CliError(`docker compose stop failed for Tenant ${tenant.name} (exit ${code}).`, 1);
649
595
  stopped.push(tenant.name);
650
596
  }
651
- const proxy = projects.get(PROXY_PROJECT) === "running" && (await stopProxy(deps.docker));
652
597
  deps.out(stopped.length
653
- ? `Stopped Tenants ${stopped.join(", ")}${proxy ? " and the Studio proxy" : ""}; their data is kept.`
654
- : `No Tenant was running${proxy ? "; stopped the Studio proxy" : ""}.`);
598
+ ? `Stopped Tenants ${stopped.join(", ")}; their data is kept.`
599
+ : "No Tenant was running.");
655
600
  return 0;
656
601
  }
657
- async function studioUrls(ctx, port) {
658
- const url = await studioUrl(ctx, port);
659
- const direct = studioOrigin(port);
660
- return url === direct ? { url } : { url: direct, proxyUrl: url };
661
- }
662
602
  async function stackStatus(ctx) {
663
603
  const base = {
664
604
  name: ctx.name,
@@ -703,7 +643,7 @@ async function stackStatus(ctx) {
703
643
  },
704
644
  ...(tenant ? { tenant } : {}),
705
645
  studio: {
706
- ...(persisted.studioPort ? await studioUrls(ctx, persisted.studioPort) : {}),
646
+ ...(persisted.studioPort ? { url: studioOrigin(persisted.studioPort) } : {}),
707
647
  state: studio ? [studio.state, studio.health].filter(Boolean).join(", ") : "absent",
708
648
  ...(persisted.studioFrameAncestors
709
649
  ? { embedOrigins: persisted.studioFrameAncestors }
@@ -719,13 +659,9 @@ async function stackStatus(ctx) {
719
659
  services,
720
660
  };
721
661
  }
722
- /** The Studio proxy, as data (`nylorun doctor`). */
723
- export async function readProxyReport(deps) {
724
- return await readProxyStatus(deps.docker, nylorunRoot(deps), deps.env);
725
- }
726
662
  /** What `nylorun status` reports, as data (`nylorun doctor`). */
727
663
  export async function readStackStatus(deps, options = {}) {
728
- await moveStackRoots(nylorunRoot(deps), deps.err);
664
+ await tidyMachine(deps);
729
665
  return await stackStatus(await selectStack(deps, options));
730
666
  }
731
667
  function describeTenant(tenant) {
@@ -760,8 +696,7 @@ async function status(deps, args) {
760
696
  out(`Runtime ${result.runtime.url ?? "?"} ${runtimeDetail}`);
761
697
  if (result.runtime.adminUrl)
762
698
  out(`Admin API ${result.runtime.adminUrl} (operators only, never proxied)`);
763
- const also = result.studio.proxyUrl ? `also ${result.studio.url}; ` : "";
764
- out(`Studio ${result.studio.proxyUrl ?? result.studio.url ?? "?"} ${result.studio.state} (${also}log in with "nylorun studio")`);
699
+ out(`Studio ${result.studio.url ?? "?"} ${result.studio.state} (log in with "nylorun studio")`);
765
700
  if (result.studio.embedOrigins?.length)
766
701
  out(`Embeds ${result.studio.embedOrigins.join(" ")} (may show Studio in a frame)`);
767
702
  out(`Gateway ${result.gateway.state} (the Model Gate; model calls fail while it is down)`);
@@ -848,7 +783,6 @@ async function ls(deps, args) {
848
783
  const memory = [...(projects?.values() ?? [])].includes("running")
849
784
  ? await tenantMemory(deps.docker)
850
785
  : undefined;
851
- const proxy = proxyDisabled(deps.env) ? undefined : await readProxySettings(base);
852
786
  const tenants = [];
853
787
  for (const entry of await listTenants(base)) {
854
788
  const persisted = (await readStackEnv(stackPaths(entry.root))) ?? {};
@@ -862,13 +796,7 @@ async function ls(deps, args) {
862
796
  ...(persisted.runtimePort
863
797
  ? { runtimeUrl: `http://${STACK_CLIENT_HOST}:${persisted.runtimePort}` }
864
798
  : {}),
865
- ...(persisted.studioPort
866
- ? {
867
- studioUrl: proxy
868
- ? proxyOrigin(entry.name, proxy.port)
869
- : studioOrigin(persisted.studioPort),
870
- }
871
- : {}),
799
+ ...(persisted.studioPort ? { studioUrl: studioOrigin(persisted.studioPort) } : {}),
872
800
  ports: {
873
801
  ...(persisted.runtimePort ? { runtime: persisted.runtimePort } : {}),
874
802
  ...(persisted.adminPort ? { admin: persisted.adminPort } : {}),
@@ -920,7 +848,6 @@ async function deleteStack(deps, args) {
920
848
  ? composeArgs(ctx, "down", "--volumes", "--remove-orphans")
921
849
  : ["compose", "--project-name", ctx.project, "down", "--volumes", "--remove-orphans"]);
922
850
  await rm(ctx.paths.root, { recursive: true, force: true });
923
- await refreshProxyRoutes(deps.docker, base, await proxyRoutes(deps));
924
851
  deps.out(`Deleted Tenant ${name}: its containers, volumes and Host root (vault key included).`);
925
852
  return 0;
926
853
  }
@@ -955,7 +882,7 @@ async function runningStack(ctx, options) {
955
882
  hostId: host.hostId,
956
883
  adminKey,
957
884
  studioPort: persisted.studioPort,
958
- studioUrl: await studioUrl(ctx, persisted.studioPort),
885
+ studioUrl: studioOrigin(persisted.studioPort),
959
886
  studioUp,
960
887
  started: false,
961
888
  };
@@ -1005,7 +932,7 @@ export function tenantStudioPath(tenantId) {
1005
932
  * Undefined (after a warning) when Studio does not answer.
1006
933
  */
1007
934
  export async function studioLoginUrl(deps, running, next) {
1008
- const login = await tryStudioLogin({ deps }, { port: running.studioPort, url: running.studioUrl }, running.adminKey);
935
+ const login = await tryStudioLogin({ deps }, running.studioPort, running.adminKey);
1009
936
  return login === undefined ? undefined : withNext(login, next);
1010
937
  }
1011
938
  async function studio(deps, args, options = {}) {
@@ -1035,7 +962,6 @@ async function studio(deps, args, options = {}) {
1035
962
  deps.out(`Studio ${login}`);
1036
963
  return 0;
1037
964
  }
1038
- // The login's origin: the proxy's, or Studio's own port when the proxy did not answer.
1039
965
  const origin = new URL(login).origin;
1040
966
  deps.out(`Studio ${next ? new URL(next, origin).toString() : origin}`);
1041
967
  await openLogin({ deps }, login);
@@ -1043,7 +969,7 @@ async function studio(deps, args, options = {}) {
1043
969
  }
1044
970
  /** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
1045
971
  export async function runStudioCommand(args, deps, options = {}) {
1046
- await moveStackRoots(nylorunRoot(deps), deps.err);
972
+ await tidyMachine(deps);
1047
973
  return await studio(deps, args, options);
1048
974
  }
1049
975
  const COMMANDS = {
@@ -1067,6 +993,6 @@ export async function runStackCommand(name, args, deps) {
1067
993
  const command = COMMANDS[name];
1068
994
  if (!command)
1069
995
  throw usageError(`Unknown command ${name}.\n${stackUsage}`);
1070
- await moveStackRoots(nylorunRoot(deps), deps.err);
996
+ await tidyMachine(deps);
1071
997
  return await command(deps, args);
1072
998
  }
@@ -6,7 +6,7 @@
6
6
  * every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
7
7
  * `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
8
8
  * the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
9
- * UID/GID, the Host root, the images, Studio's public origins) comes from `docker/.env`.
9
+ * UID/GID, the Host root, the images) comes from `docker/.env`.
10
10
  *
11
11
  * The Tenant's state is its Postgres database, executed through Restate, with its
12
12
  * history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
@@ -7,7 +7,7 @@ import { PINNED_IMAGES } from "./images.js";
7
7
  * every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
8
8
  * `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
9
9
  * the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
10
- * UID/GID, the Host root, the images, Studio's public origins) comes from `docker/.env`.
10
+ * UID/GID, the Host root, the images) comes from `docker/.env`.
11
11
  *
12
12
  * The Tenant's state is its Postgres database, executed through Restate, with its
13
13
  * history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
@@ -72,9 +72,6 @@ services:
72
72
  command: ["--node-name=restate-1"] # stable name, so data is found on restart
73
73
  environment:
74
74
  RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE: /run/nylorun/restate-identity.pem
75
- # RocksDB's memory (block cache and memtables) defaults to 2 GiB; a local Tenant needs a
76
- # fraction of it, and several Tenants share a laptop.
77
- RESTATE_ROCKSDB_TOTAL_MEMORY_SIZE: 256MiB
78
75
  volumes:
79
76
  - restate:/restate-data
80
77
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/docker/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
@@ -209,10 +206,8 @@ services:
209
206
  NYLORUN_RUNTIME_URL: http://runtime:4001 # the operator listener: Admin and Tenant API
210
207
  NYLORUN_ADMIN_KEY_FILE: /run/nylorun/host-credentials.json
211
208
  PORT: "3000"
212
- # Studio's Host check accepts localhost/127.0.0.1 on the published port, and the
213
- # proxy's origin (http://<name>.localhost:<port>), kept in .env.
209
+ # Studio's Host check accepts localhost/127.0.0.1 on the published port.
214
210
  NYLORUN_STUDIO_PUBLIC_PORT: \${NYLORUN_STUDIO_PORT}
215
- NYLORUN_STUDIO_PUBLIC_ORIGINS: \${NYLORUN_STUDIO_PUBLIC_ORIGINS:-}
216
211
  # Browsers share cookies across ports of one host: a cookie per Tenant keeps the
217
212
  # sessions of two Studios on localhost apart.
218
213
  NYLORUN_STUDIO_SESSION_COOKIE: nylorun_studio_${name}
@@ -34,11 +34,6 @@ export interface StackEnv {
34
34
  * when telemetry is off. Decided again on every start.
35
35
  */
36
36
  studioAnalyticsId: string;
37
- /**
38
- * Origins Studio also serves besides localhost and 127.0.0.1 on its port, separated by
39
- * spaces: the proxy's `http://<name>.localhost:<port>`, or empty. Decided on every start.
40
- */
41
- studioPublicOrigins: string;
42
37
  /** The Tenant's name, which the Runtime gives the Tenant it creates. */
43
38
  tenantName: string;
44
39
  /**
@@ -24,7 +24,6 @@ const KEYS = {
24
24
  studioImage: "NYLORUN_STUDIO_IMAGE",
25
25
  studioFrameAncestors: "NYLORUN_STUDIO_FRAME_ANCESTORS",
26
26
  studioAnalyticsId: "NYLORUN_STUDIO_ANALYTICS_ID",
27
- studioPublicOrigins: "NYLORUN_STUDIO_PUBLIC_ORIGINS",
28
27
  tenantName: "NYLORUN_TENANT_NAME",
29
28
  derivedPrincipals: "NYLORUN_DERIVED_PRINCIPALS",
30
29
  };
@@ -82,10 +81,6 @@ export function renderEnvFile(env) {
82
81
  "# it off with nylorun telemetry disable or NYLORUN_TELEMETRY_DISABLED=1.",
83
82
  line("studioAnalyticsId"),
84
83
  "",
85
- "# Studio's address through the proxy (http://<name>.localhost:<port>); empty",
86
- "# without the proxy. Set on every start.",
87
- line("studioPublicOrigins"),
88
- "",
89
84
  "# The Tenant's name, which the Runtime gives the Tenant it creates on the first",
90
85
  "# start, and the derived principals it registers on that Tenant (keys derived",
91
86
  "# from the admin key; `project` is the Project link's).",
@@ -2,13 +2,12 @@
2
2
  * Images a local Tenant runs. The Runtime and Studio tags are pinned by this
3
3
  * CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
4
4
  * (local builds, CI images). Postgres, Restate and s2-lite are the official
5
- * images, pinned here, as is Caddy for the machine's Studio proxy.
5
+ * images, pinned here.
6
6
  */
7
7
  export declare const PINNED_IMAGES: {
8
8
  readonly postgres: "postgres:17.11";
9
9
  readonly restate: "docker.restate.dev/restatedev/restate:1.7.12";
10
10
  readonly s2: "ghcr.io/s2-streamstore/s2:0.43.0";
11
- readonly proxy: "caddy:2.11.6";
12
11
  };
13
12
  export interface StackImages {
14
13
  runtime: string;
@@ -16,7 +15,6 @@ export interface StackImages {
16
15
  postgres: string;
17
16
  restate: string;
18
17
  s2: string;
19
- proxy: string;
20
18
  }
21
19
  /** `NYLORUN_RUNTIME_IMAGE` names the Runtime image, so its version is unknown here. */
22
20
  export declare function runtimeImageOverridden(env: Readonly<Record<string, string | undefined>>): boolean;
@@ -3,13 +3,12 @@ import { pinnedVersion } from "./versions.js";
3
3
  * Images a local Tenant runs. The Runtime and Studio tags are pinned by this
4
4
  * CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
5
5
  * (local builds, CI images). Postgres, Restate and s2-lite are the official
6
- * images, pinned here, as is Caddy for the machine's Studio proxy.
6
+ * images, pinned here.
7
7
  */
8
8
  export const PINNED_IMAGES = {
9
9
  postgres: "postgres:17.11",
10
10
  restate: "docker.restate.dev/restatedev/restate:1.7.12",
11
11
  s2: "ghcr.io/s2-streamstore/s2:0.43.0",
12
- proxy: "caddy:2.11.6",
13
12
  };
14
13
  function override(env, name) {
15
14
  const value = env[name]?.trim();
@@ -1,9 +1,8 @@
1
1
  import { type StackDeps } from "./commands.js";
2
- export { ensureStack, isStackCommand, readProxyReport, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
2
+ export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
3
3
  export type { StackDeps, StackEndpoints, StackStatus, StackTenant } from "./commands.js";
4
4
  export { checkDocker } from "./docker.js";
5
5
  export type { Check, DockerChecks } from "./docker.js";
6
- export type { ProxyStatus } from "./proxy.js";
7
6
  /** Real dependencies: docker on PATH, global fetch, the terminal. */
8
7
  export declare function defaultStackDeps(env: Readonly<Record<string, string | undefined>>): StackDeps;
9
8
  /** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete`. */
@@ -4,7 +4,7 @@ import { runStackCommand, runStudioCommand } from "./commands.js";
4
4
  import { spawnDocker } from "./docker.js";
5
5
  import { pinnedVersion } from "./versions.js";
6
6
  import { loopbackPorts } from "./ports.js";
7
- export { ensureStack, isStackCommand, readProxyReport, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
7
+ export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
8
8
  export { checkDocker } from "./docker.js";
9
9
  function browserCommand(env) {
10
10
  if (process.platform === "darwin")
@@ -21,8 +21,6 @@ const STACK_ENV_OWNED = [
21
21
  "NYLORUN_TENANT_NAME",
22
22
  "NYLORUN_DERIVED_PRINCIPALS",
23
23
  "NYLORUN_STUDIO_ANALYTICS_ID",
24
- "NYLORUN_STUDIO_PUBLIC_ORIGINS",
25
- "NYLORUN_PROXY_PORT",
26
24
  ];
27
25
  /** Real dependencies: docker on PATH, global fetch, the terminal. */
28
26
  export function defaultStackDeps(env) {
@@ -1,6 +1,4 @@
1
1
  export declare const DEFAULT_PORTS: {
2
- /** The Studio proxy, one per machine (`http://<tenant>.localhost:<port>`). */
3
- readonly proxy: 4160;
4
2
  readonly runtime: 8787;
5
3
  /** The Runtime's operator listener (Admin API). */
6
4
  readonly admin: 8788;
@@ -1,7 +1,5 @@
1
1
  import { createServer } from "node:net";
2
2
  export const DEFAULT_PORTS = {
3
- /** The Studio proxy, one per machine (`http://<tenant>.localhost:<port>`). */
4
- proxy: 4160,
5
3
  runtime: 8787,
6
4
  /** The Runtime's operator listener (Admin API). */
7
5
  admin: 8788,
@@ -43,6 +43,4 @@ export declare function prepareStack(input: {
43
43
  };
44
44
  /** The measurement id Studio reports page views to; absent when telemetry is off. */
45
45
  studioAnalyticsId?: string;
46
- /** Studio's origin through the proxy; absent without the proxy. */
47
- studioPublicOrigin?: string;
48
46
  }): Promise<PreparedStack>;
@@ -77,7 +77,6 @@ export async function prepareStack(input) {
77
77
  studioImage: input.images.studio,
78
78
  studioFrameAncestors,
79
79
  studioAnalyticsId: input.studioAnalyticsId ?? "",
80
- studioPublicOrigins: input.studioPublicOrigin ?? "",
81
80
  tenantName: input.name,
82
81
  derivedPrincipals: (input.derivedPrincipals?.trim()
83
82
  ? parseDerivedPrincipals(input.derivedPrincipals, "NYLORUN_DERIVED_PRINCIPALS")
@@ -1,3 +1,4 @@
1
+ import type { DockerRunner } from "./docker.js";
1
2
  /**
2
3
  * The machine's local Tenants (tenancy.md §6): one installation per Tenant, each with its own
3
4
  * Host root `~/.nylorun/tenants/<name>/`, Compose project `nylorun-<name>`, ports and volumes.
@@ -48,3 +49,10 @@ export declare function chooseTenantName(base: string, projectDir: string): Prom
48
49
  * volumes with new keys.
49
50
  */
50
51
  export declare function moveStackRoots(base: string, err: (line: string) => void): Promise<void>;
52
+ /**
53
+ * Remove the Studio proxy of nylorun 0.6 (a Caddy container `nylorun-proxy` on its own network,
54
+ * files in `~/.nylorun/proxy/`). Removing the container also detaches it from the Tenants'
55
+ * networks, so their `docker compose down` works again. The directory goes last, once Docker
56
+ * removed both (or never had them), so a Docker that does not answer is retried next time.
57
+ */
58
+ export declare function removeOldProxy(docker: DockerRunner, base: string, err: (line: string) => void): Promise<void>;
@@ -148,3 +148,22 @@ export async function moveStackRoots(base, err) {
148
148
  err(`Moved Tenants ${moved.join(", ")} from ${tilde(stacks)} to ${tilde(tenantsDir(base))}.`);
149
149
  }
150
150
  }
151
+ /**
152
+ * Remove the Studio proxy of nylorun 0.6 (a Caddy container `nylorun-proxy` on its own network,
153
+ * files in `~/.nylorun/proxy/`). Removing the container also detaches it from the Tenants'
154
+ * networks, so their `docker compose down` works again. The directory goes last, once Docker
155
+ * removed both (or never had them), so a Docker that does not answer is retried next time.
156
+ */
157
+ export async function removeOldProxy(docker, base, err) {
158
+ const dir = join(base, "proxy");
159
+ if (!existsSync(dir))
160
+ return;
161
+ const container = await docker.run(["rm", "--force", "nylorun-proxy"]);
162
+ if (container.code !== 0 && !/no such container/i.test(container.stderr))
163
+ return;
164
+ const network = await docker.run(["network", "rm", "nylorun-proxy"]);
165
+ if (network.code !== 0 && !/not found|no such network/i.test(network.stderr))
166
+ return;
167
+ await rm(dir, { recursive: true, force: true });
168
+ err("Removed the Studio proxy of nylorun 0.6 (nylorun-proxy); each Studio is on its own localhost port again (\"nylorun studio\" opens it).");
169
+ }
@@ -3,8 +3,8 @@ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>
3
3
  export declare function studioOrigin(port: number): string;
4
4
  /**
5
5
  * Ask the Studio container for a single-use login token with the admin key
6
- * (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port, or
7
- * the proxy's `http://<name>.localhost:<port>`) and return the URL to open there.
6
+ * (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
7
+ * and return the URL to open there.
8
8
  * Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
9
9
  */
10
10
  export declare function mintStudioLogin(input: {
@@ -5,8 +5,8 @@ export function studioOrigin(port) {
5
5
  }
6
6
  /**
7
7
  * Ask the Studio container for a single-use login token with the admin key
8
- * (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port, or
9
- * the proxy's `http://<name>.localhost:<port>`) and return the URL to open there.
8
+ * (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
9
+ * and return the URL to open there.
10
10
  * Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
11
11
  */
12
12
  export async function mintStudioLogin(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.6.0-beta",
3
+ "version": "0.7.0-beta",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose",
@@ -42,6 +42,6 @@
42
42
  },
43
43
  "nylorun": {
44
44
  "runtime": "0.15.2-beta",
45
- "studio": "0.17.0-beta"
45
+ "studio": "0.18.0-beta"
46
46
  }
47
47
  }
@@ -1,78 +0,0 @@
1
- import type { DockerRunner } from "./docker.js";
2
- import { type PortProbe } from "./ports.js";
3
- /**
4
- * The Studio proxy: one small Caddy container per machine that gives each local Tenant's
5
- * Studio the address `http://<name>.localhost:<port>`. Browsers keep cookies per host, not
6
- * per port, so separate hosts keep the Studios' sessions apart. It routes browsers to Studio
7
- * only (programs keep the Runtime's `http://localhost:<port>`: Node's resolver need not
8
- * resolve `*.localhost`) and holds no Tenant data: its files are `~/.nylorun/proxy/`
9
- * (`compose.yaml`, `Caddyfile`, `.env`), and Caddy's own state is tmpfs.
10
- *
11
- * The proxy joins each Tenant's network (named after its Compose project) and reaches Studio
12
- * by container name, `<project>-studio:3000`. Docker cannot remove a network the proxy is
13
- * attached to, so `reset` and `delete` disconnect it first.
14
- */
15
- export declare const PROXY_PROJECT = "nylorun-proxy";
16
- export declare const PROXY_CONTAINER = "nylorun-proxy";
17
- export interface ProxyPaths {
18
- dir: string;
19
- compose: string;
20
- caddyfile: string;
21
- /** The port, and whether Docker took the `::1` binding. */
22
- env: string;
23
- }
24
- export declare function proxyPaths(base: string): ProxyPaths;
25
- /** `NYLORUN_PROXY_DISABLED=1`: no proxy; Studio is `http://localhost:<port>`. */
26
- export declare function proxyDisabled(env: Readonly<Record<string, string | undefined>>): boolean;
27
- export declare function proxyOrigin(name: string, port: number): string;
28
- export interface ProxySettings {
29
- port: number;
30
- /** Also published on `[::1]` (false once Docker refused it). */
31
- ipv6: boolean;
32
- }
33
- export declare function readProxySettings(base: string): Promise<ProxySettings | undefined>;
34
- /**
35
- * The proxy's settings. On first use its port is 4160 when free, else a free one outside
36
- * `taken`; it is kept after that.
37
- */
38
- export declare function ensureProxySettings(base: string, ports: PortProbe, taken: ReadonlySet<number>): Promise<ProxySettings>;
39
- /** A Tenant the proxy routes to: its name and Compose project (also its network's name). */
40
- export interface ProxyRoute {
41
- name: string;
42
- project: string;
43
- }
44
- /**
45
- * One site per Tenant. Site addresses carry no port: Caddy listens on 80 in the container,
46
- * and its host match ignores the published port in the browser's Host. The browser's Host
47
- * (`<name>.localhost:<port>`) reaches Studio unchanged, which Studio checks; a Tenant that is
48
- * not running answers 502, and an unknown host 404.
49
- */
50
- export declare function renderCaddyfile(routes: readonly ProxyRoute[]): string;
51
- export declare function renderProxyCompose(ipv6: boolean): string;
52
- /** Detach the proxy from a Tenant's network, so Compose can remove it; never fails. */
53
- export declare function disconnectProxy(docker: DockerRunner, network: string): Promise<void>;
54
- /**
55
- * Write the routes, bring the proxy up (Compose recreates it when its file changed), attach
56
- * it to every routed Tenant's network and reload Caddy. Never throws: a problem is warned
57
- * once and the caller shows Studio's own port. When Docker refuses `[::1]` (IPv6 off), the
58
- * proxy listens on 127.0.0.1 only, and says so once.
59
- */
60
- export declare function startProxy(input: {
61
- docker: DockerRunner;
62
- base: string;
63
- settings: ProxySettings;
64
- routes: readonly ProxyRoute[];
65
- err(line: string): void;
66
- pollMs?: number;
67
- }): Promise<boolean>;
68
- /** Rewrite the routes of an existing proxy and reload it (`delete`); never fails. */
69
- export declare function refreshProxyRoutes(docker: DockerRunner, base: string, routes: readonly ProxyRoute[]): Promise<void>;
70
- /** Stop the proxy (`stop --all`); true when it stopped. */
71
- export declare function stopProxy(docker: DockerRunner): Promise<boolean>;
72
- export interface ProxyStatus {
73
- state: "running" | "stopped" | "absent" | "disabled";
74
- port?: number;
75
- ipv6?: boolean;
76
- }
77
- /** The proxy as `nylorun doctor` reports it. */
78
- export declare function readProxyStatus(docker: DockerRunner, base: string, env: Readonly<Record<string, string | undefined>>): Promise<ProxyStatus>;
@@ -1,224 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import { existsSync } from "node:fs";
3
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { parseEnvLines } from "./env-file.js";
6
- import { PINNED_IMAGES } from "./images.js";
7
- import { choosePort, DEFAULT_PORTS } from "./ports.js";
8
- /**
9
- * The Studio proxy: one small Caddy container per machine that gives each local Tenant's
10
- * Studio the address `http://<name>.localhost:<port>`. Browsers keep cookies per host, not
11
- * per port, so separate hosts keep the Studios' sessions apart. It routes browsers to Studio
12
- * only (programs keep the Runtime's `http://localhost:<port>`: Node's resolver need not
13
- * resolve `*.localhost`) and holds no Tenant data: its files are `~/.nylorun/proxy/`
14
- * (`compose.yaml`, `Caddyfile`, `.env`), and Caddy's own state is tmpfs.
15
- *
16
- * The proxy joins each Tenant's network (named after its Compose project) and reaches Studio
17
- * by container name, `<project>-studio:3000`. Docker cannot remove a network the proxy is
18
- * attached to, so `reset` and `delete` disconnect it first.
19
- */
20
- export const PROXY_PROJECT = "nylorun-proxy";
21
- export const PROXY_CONTAINER = "nylorun-proxy";
22
- export function proxyPaths(base) {
23
- const dir = join(base, "proxy");
24
- return {
25
- dir,
26
- compose: join(dir, "compose.yaml"),
27
- caddyfile: join(dir, "Caddyfile"),
28
- env: join(dir, ".env"),
29
- };
30
- }
31
- /** `NYLORUN_PROXY_DISABLED=1`: no proxy; Studio is `http://localhost:<port>`. */
32
- export function proxyDisabled(env) {
33
- return /^(1|true)$/i.test(env.NYLORUN_PROXY_DISABLED?.trim() ?? "");
34
- }
35
- export function proxyOrigin(name, port) {
36
- return `http://${name}.localhost:${port}`;
37
- }
38
- export async function readProxySettings(base) {
39
- let text;
40
- try {
41
- text = await readFile(proxyPaths(base).env, "utf8");
42
- }
43
- catch {
44
- return undefined;
45
- }
46
- const values = parseEnvLines(text);
47
- const port = Number(values.get("NYLORUN_PROXY_PORT"));
48
- if (!Number.isInteger(port) || port < 1 || port > 65535)
49
- return undefined;
50
- return { port, ipv6: values.get("NYLORUN_PROXY_IPV6") !== "false" };
51
- }
52
- async function writeAtomic(path, text) {
53
- const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
54
- await writeFile(temporary, text, { mode: 0o644 });
55
- await rename(temporary, path);
56
- }
57
- async function writeProxySettings(base, settings) {
58
- const paths = proxyPaths(base);
59
- await mkdir(paths.dir, { recursive: true, mode: 0o755 });
60
- await writeAtomic(paths.env, [
61
- "# Written by `nylorun start`: the Studio proxy's port, kept from its first start,",
62
- "# and whether it is also published on [::1].",
63
- `NYLORUN_PROXY_PORT=${settings.port}`,
64
- `NYLORUN_PROXY_IPV6=${settings.ipv6}`,
65
- "",
66
- ].join("\n"));
67
- }
68
- /**
69
- * The proxy's settings. On first use its port is 4160 when free, else a free one outside
70
- * `taken`; it is kept after that.
71
- */
72
- export async function ensureProxySettings(base, ports, taken) {
73
- const existing = await readProxySettings(base);
74
- if (existing)
75
- return existing;
76
- const settings = { port: await choosePort(ports, DEFAULT_PORTS.proxy, undefined, taken), ipv6: true };
77
- await writeProxySettings(base, settings);
78
- return settings;
79
- }
80
- /**
81
- * One site per Tenant. Site addresses carry no port: Caddy listens on 80 in the container,
82
- * and its host match ignores the published port in the browser's Host. The browser's Host
83
- * (`<name>.localhost:<port>`) reaches Studio unchanged, which Studio checks; a Tenant that is
84
- * not running answers 502, and an unknown host 404.
85
- */
86
- export function renderCaddyfile(routes) {
87
- const sites = routes.map(({ name, project }) => `http://${name}.localhost {
88
- reverse_proxy ${project}-studio:3000
89
- handle_errors {
90
- respond \`Tenant ${name} is not running. Run "nylorun start --tenant ${name}".\` 502
91
- }
92
- }
93
- `);
94
- return [
95
- `# Written by \`nylorun start\`: a site per Tenant under ~/.nylorun/tenants. Rewritten on every start.
96
- {
97
- auto_https off
98
- admin localhost:2019 # inside the container only; \`caddy reload\` uses it
99
- }
100
- `,
101
- ...sites,
102
- `:80 {
103
- respond \`No Tenant at {host}. "nylorun ls" lists the Tenants on this machine.\` 404
104
- }
105
- `,
106
- ].join("\n");
107
- }
108
- export function renderProxyCompose(ipv6) {
109
- return `# Written by \`nylorun start\`; rewritten on every start. The port lives in .env.
110
- name: ${PROXY_PROJECT}
111
-
112
- services:
113
- proxy: # http://<tenant>.localhost:<port>: every local Tenant's Studio
114
- image: ${PINNED_IMAGES.proxy}
115
- container_name: ${PROXY_CONTAINER}
116
- labels:
117
- dev.nylorun.proxy: "true"
118
- volumes:
119
- - .:/etc/caddy:ro # this directory: the Caddyfile, reloaded with \`caddy reload\`
120
- tmpfs: [/config, /data] # Caddy's own state; nothing to keep
121
- ports:
122
- - "127.0.0.1:\${NYLORUN_PROXY_PORT:?run nylorun start}:80"${ipv6
123
- ? `
124
- - "[::1]:\${NYLORUN_PROXY_PORT:?run nylorun start}:80" # macOS resolves <name>.localhost to ::1 only`
125
- : ""}
126
- restart: unless-stopped
127
-
128
- networks:
129
- default:
130
- name: ${PROXY_PROJECT}
131
- labels:
132
- dev.nylorun.proxy: "true"
133
- `;
134
- }
135
- function proxyCompose(paths, ...args) {
136
- return ["compose", "--project-name", PROXY_PROJECT, "--file", paths.compose, "--env-file", paths.env, ...args];
137
- }
138
- function lastLine(text) {
139
- return text.trim().split(/\r?\n/).at(-1) ?? "";
140
- }
141
- /** Attach the proxy to a Tenant's network; "already attached" and "no such network" are fine. */
142
- async function connectProxy(docker, network) {
143
- const result = await docker.run(["network", "connect", network, PROXY_CONTAINER]);
144
- if (result.code === 0 || /already exists|not found|no such network/i.test(result.stderr))
145
- return undefined;
146
- return lastLine(result.stderr) || `exit ${result.code}`;
147
- }
148
- /** Detach the proxy from a Tenant's network, so Compose can remove it; never fails. */
149
- export async function disconnectProxy(docker, network) {
150
- await docker.run(["network", "disconnect", "--force", network, PROXY_CONTAINER]);
151
- }
152
- async function reloadProxy(docker) {
153
- return await docker.run(["exec", PROXY_CONTAINER, "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]);
154
- }
155
- /**
156
- * Write the routes, bring the proxy up (Compose recreates it when its file changed), attach
157
- * it to every routed Tenant's network and reload Caddy. Never throws: a problem is warned
158
- * once and the caller shows Studio's own port. When Docker refuses `[::1]` (IPv6 off), the
159
- * proxy listens on 127.0.0.1 only, and says so once.
160
- */
161
- export async function startProxy(input) {
162
- const { docker, base, err } = input;
163
- const paths = proxyPaths(base);
164
- const warn = (problem) => err(`Warning: the Studio proxy is not available (${problem}); Studio is on its own port. See "nylorun doctor".`);
165
- try {
166
- await writeAtomic(paths.caddyfile, renderCaddyfile(input.routes));
167
- await writeAtomic(paths.compose, renderProxyCompose(input.settings.ipv6));
168
- }
169
- catch (error) {
170
- warn(error instanceof Error ? error.message : String(error));
171
- return false;
172
- }
173
- let up = await docker.run(proxyCompose(paths, "up", "--detach"));
174
- if (up.code !== 0 && input.settings.ipv6) {
175
- await writeAtomic(paths.compose, renderProxyCompose(false));
176
- const ipv4 = await docker.run(proxyCompose(paths, "up", "--detach"));
177
- if (ipv4.code === 0) {
178
- await writeProxySettings(base, { ...input.settings, ipv6: false });
179
- err(`Docker refused the Studio proxy on [::1]:${input.settings.port} (${lastLine(up.stderr)}); it listens on 127.0.0.1 only, so a browser that resolves *.localhost to ::1 alone does not reach it.`);
180
- }
181
- up = ipv4;
182
- }
183
- if (up.code !== 0) {
184
- warn(`docker compose up: ${lastLine(up.stderr) || `exit ${up.code}`}`);
185
- return false;
186
- }
187
- for (const route of input.routes) {
188
- const problem = await connectProxy(docker, route.project);
189
- if (problem)
190
- err(`Warning: the Studio proxy could not join ${route.project} (${problem}).`);
191
- }
192
- // A container Compose just created may not take the reload yet.
193
- for (let attempt = 1;; attempt += 1) {
194
- const reload = await reloadProxy(docker);
195
- if (reload.code === 0)
196
- return true;
197
- if (attempt === 10) {
198
- warn(`caddy reload: ${lastLine(reload.stderr) || `exit ${reload.code}`}`);
199
- return false;
200
- }
201
- await new Promise((resolve) => setTimeout(resolve, input.pollMs ?? 500));
202
- }
203
- }
204
- /** Rewrite the routes of an existing proxy and reload it (`delete`); never fails. */
205
- export async function refreshProxyRoutes(docker, base, routes) {
206
- const paths = proxyPaths(base);
207
- if (!existsSync(paths.caddyfile))
208
- return;
209
- await writeAtomic(paths.caddyfile, renderCaddyfile(routes));
210
- await reloadProxy(docker);
211
- }
212
- /** Stop the proxy (`stop --all`); true when it stopped. */
213
- export async function stopProxy(docker) {
214
- return (await docker.run(["compose", "--project-name", PROXY_PROJECT, "stop"])).code === 0;
215
- }
216
- /** The proxy as `nylorun doctor` reports it. */
217
- export async function readProxyStatus(docker, base, env) {
218
- if (proxyDisabled(env))
219
- return { state: "disabled" };
220
- const settings = await readProxySettings(base);
221
- const inspect = await docker.run(["inspect", "--format", "{{.State.Status}}", PROXY_CONTAINER]);
222
- const state = inspect.code !== 0 ? "absent" : inspect.stdout.trim() === "running" ? "running" : "stopped";
223
- return { state, ...(settings ? { port: settings.port, ipv6: settings.ipv6 } : {}) };
224
- }