moshcode 0.24.0

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +580 -0
  3. package/bin/moshcode.mjs +674 -0
  4. package/bin/moshscript.mjs +29 -0
  5. package/examples/alive.mosh +6 -0
  6. package/examples/scripting-the-cli.mosh +21 -0
  7. package/examples/team-secrets.mosh +20 -0
  8. package/examples/templates/bun-caddy-sqlite/.env.example +14 -0
  9. package/examples/templates/bun-caddy-sqlite/Caddyfile +18 -0
  10. package/examples/templates/bun-caddy-sqlite/README.md +97 -0
  11. package/examples/templates/bun-caddy-sqlite/deploy/moshcode-dns.service +39 -0
  12. package/examples/templates/bun-caddy-sqlite/deploy/moshpit-service.service +38 -0
  13. package/examples/templates/bun-caddy-sqlite/package.json +15 -0
  14. package/examples/templates/bun-caddy-sqlite/src/db.ts +47 -0
  15. package/examples/templates/bun-caddy-sqlite/src/server.ts +44 -0
  16. package/examples/templates/bun-caddy-sqlite/template.json +10 -0
  17. package/examples/templates/caddy-proxy/Caddyfile +36 -0
  18. package/examples/templates/caddy-proxy/README.md +104 -0
  19. package/examples/templates/caddy-proxy/deploy/moshcode-dns.service +39 -0
  20. package/examples/templates/caddy-proxy/template.json +8 -0
  21. package/examples/templates/caddy-static/Caddyfile +16 -0
  22. package/examples/templates/caddy-static/README.md +90 -0
  23. package/examples/templates/caddy-static/deploy/moshcode-dns.service +39 -0
  24. package/examples/templates/caddy-static/site/index.html +11 -0
  25. package/examples/templates/caddy-static/template.json +8 -0
  26. package/install.sh +194 -0
  27. package/package.json +28 -0
  28. package/prd/0000-template.md +49 -0
  29. package/prd/0001-wrap-ugig-and-coinpay-clis.md +121 -0
  30. package/prd/0002-separate-agent-and-raw-engine-launches.md +113 -0
  31. package/prd/0003-cross-engine-mcp-and-skill-installation.md +165 -0
  32. package/prd/0004-moshscript-run-programmable-moshcode.md +344 -0
  33. package/prd/0005-hosted-moshpit-resolver.md +192 -0
  34. package/prd/0006-help.md +359 -0
  35. package/prd/0007-profullstack-site-init.md +1183 -0
  36. package/prd/README.md +26 -0
  37. package/src/ads.mjs +58 -0
  38. package/src/auth.mjs +193 -0
  39. package/src/cli-schema.mjs +533 -0
  40. package/src/cli.mjs +118 -0
  41. package/src/commands.mjs +259 -0
  42. package/src/completion.mjs +594 -0
  43. package/src/console.mjs +244 -0
  44. package/src/dns-system.mjs +404 -0
  45. package/src/dns.mjs +2872 -0
  46. package/src/doh-server.mjs +256 -0
  47. package/src/doh.mjs +218 -0
  48. package/src/engines.mjs +385 -0
  49. package/src/escalate.mjs +85 -0
  50. package/src/help.mjs +443 -0
  51. package/src/integrations.mjs +265 -0
  52. package/src/mcp-catalog.mjs +50 -0
  53. package/src/mcp.mjs +155 -0
  54. package/src/mirror.mjs +187 -0
  55. package/src/notify.mjs +86 -0
  56. package/src/open-url.mjs +34 -0
  57. package/src/parking-http.mjs +65 -0
  58. package/src/pins.mjs +190 -0
  59. package/src/pit-url.mjs +13 -0
  60. package/src/prd.mjs +341 -0
  61. package/src/pty.mjs +176 -0
  62. package/src/pwd.mjs +103 -0
  63. package/src/registry.mjs +37 -0
  64. package/src/release-install.mjs +191 -0
  65. package/src/runtime.mjs +161 -0
  66. package/src/selfupdate.mjs +215 -0
  67. package/src/serve.mjs +502 -0
  68. package/src/skills.mjs +93 -0
  69. package/src/tabs.mjs +144 -0
  70. package/src/templates.mjs +456 -0
  71. package/src/tools.mjs +231 -0
  72. package/src/trade.mjs +137 -0
  73. package/src/trust.mjs +712 -0
  74. package/src/tui.mjs +736 -0
  75. package/src/ui.mjs +49 -0
  76. package/src/uninstall.mjs +113 -0
  77. package/src/upgrade.mjs +217 -0
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // moshscript — thin alias for `moshcode run`, so `.mosh` files can use:
3
+ //
4
+ // #!/usr/bin/env moshscript
5
+ //
6
+ // as a shebang and run themselves like shell scripts:
7
+ //
8
+ // chmod +x deploy.mosh && ./deploy.mosh --dry-run staging
9
+ //
10
+ // All arguments are forwarded unchanged to `moshcode run`.
11
+ import { spawn } from "node:child_process";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const BIN = fileURLToPath(new URL("moshcode.mjs", import.meta.url));
15
+ const args = process.argv.slice(2); // everything after `moshscript`
16
+
17
+ const child = spawn(process.execPath, [BIN, "run", ...args], {
18
+ stdio: "inherit",
19
+ env: { ...process.env, MOSHCODE_NESTED: "1" },
20
+ });
21
+ child.on("error", (e) => { console.error(`moshscript: ${e.message}`); process.exit(1); });
22
+ child.on("exit", (code, signal) => {
23
+ if (signal) {
24
+ try { process.kill(process.pid, signal); }
25
+ catch { process.exitCode = 1; }
26
+ return;
27
+ }
28
+ process.exitCode = code ?? 0;
29
+ });
@@ -0,0 +1,6 @@
1
+ while (alive) {
2
+ code();
3
+ mosh();
4
+ notify();
5
+ repeat();
6
+ } // no bugs, only features
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env moshscript
2
+ // scripting-the-cli.mosh — secretly all JS is legal, and every CLI verb is just
3
+ // `moshcode <cmd> ...args`. Try it safely first:
4
+ //
5
+ // moshcode run examples/scripting-the-cli.mosh --dry-run
6
+ // chmod +x examples/scripting-the-cli.mosh && ./examples/scripting-the-cli.mosh claude
7
+ //
8
+ // argv[0] is the engine to drop into at the end (default: claude).
9
+
10
+ const engine = argv[0] || "claude";
11
+ const engines = ["claude", "codex"];
12
+
13
+ say(`🎸 setting up ${engines.length} engines`);
14
+ for (const e of engines) {
15
+ install(e); // → moshcode install <e>
16
+ }
17
+
18
+ mcp("install", "https://mcp.sentry.dev/mcp"); // → moshcode mcp install ...
19
+ notify(`moshcode ready — dropping into ${engine}`);
20
+
21
+ agents(engine); // → moshcode agents <engine>
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env moshscript
2
+ // team-secrets.mosh — pull a team's shared credentials, then start coding.
3
+ //
4
+ // `secrets(...)` is just `moshcode secrets ...args`, which passes through to the
5
+ // logicsrc CLI — so a script can fetch end-to-end-encrypted team secrets into a
6
+ // local .env instead of anyone passing .env files around. Try it safely first:
7
+ //
8
+ // moshcode run examples/team-secrets.mosh --dry-run acme prod claude
9
+ //
10
+ // argv: <team> <vault> [engine] (defaults: acme / prod / claude)
11
+
12
+ const team = argv[0] || "acme";
13
+ const vault = argv[1] || "prod";
14
+ const engine = argv[2] || "claude";
15
+
16
+ say(`🔐 pulling ${team}/${vault} secrets into .env (end-to-end encrypted)`);
17
+ secrets("teams", "pull", team, vault, "--env", ".env"); // → moshcode secrets teams pull <team> <vault> --env .env
18
+
19
+ say(`🎸 secrets in place — dropping into ${engine}`);
20
+ agents(engine); // → moshcode agents <engine>
@@ -0,0 +1,14 @@
1
+ # The name you registered in the Pit. Caddy matches on it; nothing resolves it
2
+ # locally.
3
+ MOSHPIT_NAME=foo.whatever
4
+
5
+ # Where Bun listens. Loopback only — Caddy is the sole client.
6
+ HOST=127.0.0.1
7
+ PORT=3000
8
+
9
+ # Leave both unset to use a local SQLite file at DB_PATH.
10
+ DB_PATH=./data/app.db
11
+
12
+ # Set both to use Turso instead. A URL without a token is refused at startup.
13
+ # TURSO_DATABASE_URL=libsql://your-db-your-org.turso.io
14
+ # TURSO_AUTH_TOKEN=
@@ -0,0 +1,18 @@
1
+ # Caddy in front of the Bun service, published at a Moshpit name.
2
+ #
3
+ # The `http://` is required and is not a style choice. A Moshpit ending is not
4
+ # in the public DNS root, so no certificate authority will issue for it — leave
5
+ # the scheme off and Caddy will try to provision a certificate, fail, and never
6
+ # bring the site up. Everything served at a Moshpit name is plain HTTP.
7
+ #
8
+ # Nothing here resolves the name. The visitor's resolver did that; by the time a
9
+ # request arrives Caddy has only a Host header to match on, which is why the
10
+ # site address must be the name exactly as it is registered.
11
+
12
+ http://{$MOSHPIT_NAME:foo.whatever} {
13
+ reverse_proxy {$APP_ADDR:127.0.0.1:3000}
14
+
15
+ log {
16
+ output file /var/log/caddy/moshpit-service.log
17
+ }
18
+ }
@@ -0,0 +1,97 @@
1
+ # bun-caddy-sqlite
2
+
3
+ Bun serving an app, Caddy in front, SQLite underneath — local file in
4
+ development, Turso in production — published at a Moshpit name over IPv6.
5
+
6
+ ## The part that surprises people
7
+
8
+ Three machines' worth of concerns, and they fail independently:
9
+
10
+ | | needs the resolver? | what it does |
11
+ |---|---|---|
12
+ | the box serving the name | **no** | Caddy matches a `Host` header, nothing more |
13
+ | the registry | — | holds the address the name points at |
14
+ | every visitor | **yes** | `sudo moshcode dns enable`, or the name resolves to nothing |
15
+
16
+ Nothing on the server ever resolves its own name. That is why there is no DNS
17
+ software in this template.
18
+
19
+ ## Locally
20
+
21
+ ```sh
22
+ bun install
23
+ cp .env.example .env
24
+ bun dev
25
+ curl -H "Host: foo.whatever" http://127.0.0.1:3000/
26
+ ```
27
+
28
+ That runs against a SQLite file at `./data/app.db`. No Turso account needed
29
+ until you deploy.
30
+
31
+ ## Deploying
32
+
33
+ 1. **Point the name at the box.** In the Pit, set `points at` to its public
34
+ IPv6 address — bare, no scheme, no brackets, no port:
35
+
36
+ ```sh
37
+ ip -6 addr show scope global | grep inet6
38
+ ```
39
+
40
+ Pick the globally routable one. An `fd..`/`fc..` address is unique-local
41
+ (Tailscale and friends live there) and the registry refuses it, because a
42
+ name pointed at one resolves somewhere only you can reach.
43
+
44
+ 2. **Move the database, if it should be hosted.** Set `TURSO_DATABASE_URL` and
45
+ `TURSO_AUTH_TOKEN` in `/etc/moshpit-service.env`. Leave them unset and the
46
+ service uses the local file under `/var/lib/moshpit-service`, which the unit
47
+ provisions. A URL without a token is refused at startup rather than failing
48
+ at the first query.
49
+
50
+ 3. **Serve it.**
51
+
52
+ ```sh
53
+ export MOSHPIT_NAME=foo.whatever
54
+ sudo cp Caddyfile /etc/caddy/Caddyfile
55
+ sudo cp deploy/moshpit-service.service /etc/systemd/system/
56
+ sudo systemctl enable --now moshpit-service caddy
57
+ sudo ufw allow 80/tcp
58
+ ```
59
+
60
+ 4. **Reach it,** on any machine that should see the name:
61
+
62
+ ```sh
63
+ sudo moshcode dns enable
64
+ sudo cp deploy/moshcode-dns.service /etc/systemd/system/ # survives reboot
65
+ sudo systemctl enable --now moshcode-dns
66
+ ```
67
+
68
+ ## Verifying, one layer at a time
69
+
70
+ A failure at any layer looks identical in a browser, so do not start there.
71
+
72
+ ```sh
73
+ # Server only — no DNS involved. Proves Caddy, the firewall, and the app.
74
+ curl -6 -H "Host: foo.whatever" http://[YOUR:V6:ADDR]/
75
+
76
+ # Resolver only. Proves the registry and the bridge.
77
+ moshcode dns resolve foo.whatever
78
+
79
+ # Both.
80
+ curl -6 http://foo.whatever/
81
+ ```
82
+
83
+ If the first works and the last does not, it is DNS. If the first fails, stop
84
+ looking at DNS.
85
+
86
+ ## Known limits
87
+
88
+ - **No HTTPS, ever.** No CA will issue for an ending outside the DNS root. That
89
+ rules out secure cookies, service workers, and WebCrypto in the browser. The
90
+ `http://` in the Caddyfile is what stops Caddy trying and failing.
91
+ - **Only machines running the resolver can reach the name.** Not phones, not a
92
+ colleague who has not installed it, not webhooks.
93
+ `pit.moshcode.sh/n/foo.whatever` is the URL for people who installed nothing.
94
+ - **One level deep.** `foo.whatever` works; `www.foo.whatever` does not.
95
+ - **Port 80 only** on the resolver path. A DNS record carries an address and has
96
+ nowhere to put a port, so a target like `[addr]:8080` only works through the
97
+ `/n/` gateway.
@@ -0,0 +1,39 @@
1
+ # The Moshpit resolver, kept running across reboots.
2
+ #
3
+ # `moshcode dns enable` sets up two halves: a systemd-resolved drop-in that
4
+ # routes Moshpit endings at the bridge, and the bridge process itself. The
5
+ # drop-in is a file and survives a reboot on its own. The process does not —
6
+ # so after a restart the routing still points at a port with nothing behind it,
7
+ # and every Moshpit name stops resolving with no obvious cause. This unit is
8
+ # the missing half.
9
+ #
10
+ # sudo cp deploy/moshcode-dns.service /etc/systemd/system/
11
+ # sudo systemctl enable --now moshcode-dns
12
+ #
13
+ # Install this on machines that need to REACH Moshpit names. A box that only
14
+ # serves one does not need it — Caddy answers whatever Host header arrives and
15
+ # never resolves its own name.
16
+
17
+ [Unit]
18
+ Description=Moshpit DNS bridge
19
+ After=network-online.target
20
+ Wants=network-online.target
21
+ Before=systemd-resolved.service
22
+
23
+ [Service]
24
+ Type=simple
25
+ # Port 5354 is unprivileged, so this does not need root. The trade-off is that
26
+ # the parking responder cannot take port 80 and falls back to the public
27
+ # parking address — which only affects names that point nowhere yet.
28
+ ExecStart=/usr/bin/env moshcode dns start --port 5354
29
+ Restart=always
30
+ RestartSec=2
31
+
32
+ DynamicUser=yes
33
+ NoNewPrivileges=yes
34
+ PrivateTmp=yes
35
+ ProtectSystem=strict
36
+ ProtectHome=yes
37
+
38
+ [Install]
39
+ WantedBy=multi-user.target
@@ -0,0 +1,38 @@
1
+ # The Bun service. Nothing Moshpit-specific here — it is a plain unit, and the
2
+ # name it answers to is Caddy's problem, not this process's.
3
+ #
4
+ # sudo cp deploy/moshpit-service.service /etc/systemd/system/
5
+ # sudo systemctl enable --now moshpit-service
6
+
7
+ [Unit]
8
+ Description=Bun service published at a Moshpit name
9
+ After=network-online.target
10
+ Wants=network-online.target
11
+
12
+ [Service]
13
+ Type=simple
14
+ WorkingDirectory=/srv/moshpit-service
15
+ ExecStart=/usr/bin/env bun src/server.ts
16
+ EnvironmentFile=-/etc/moshpit-service.env
17
+ Environment=HOST=127.0.0.1
18
+ Environment=PORT=3000
19
+
20
+ # ProtectSystem=strict makes the whole filesystem read-only, so the local
21
+ # SQLite file needs somewhere it is allowed to live. StateDirectory creates
22
+ # /var/lib/moshpit-service and hands it to the dynamic user. Unset DB_PATH and
23
+ # point TURSO_DATABASE_URL somewhere instead if the database is hosted.
24
+ StateDirectory=moshpit-service
25
+ Environment=DB_PATH=/var/lib/moshpit-service/app.db
26
+
27
+ Restart=always
28
+ RestartSec=2
29
+
30
+ # No reason for a web service to be root or to read anyone's home directory.
31
+ DynamicUser=yes
32
+ NoNewPrivileges=yes
33
+ PrivateTmp=yes
34
+ ProtectSystem=strict
35
+ ProtectHome=yes
36
+
37
+ [Install]
38
+ WantedBy=multi-user.target
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "moshpit-service",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bun --watch src/server.ts",
7
+ "start": "bun src/server.ts"
8
+ },
9
+ "dependencies": {
10
+ "@libsql/client": "^0.15.0"
11
+ },
12
+ "devDependencies": {
13
+ "@types/bun": "latest"
14
+ }
15
+ }
@@ -0,0 +1,47 @@
1
+ // One client for both shapes of the same database.
2
+ //
3
+ // Local development wants a file with nothing to sign up for; production wants
4
+ // Turso. `@libsql/client` speaks both, so the only difference is the URL — and
5
+ // keeping that difference in an env var rather than in a branch means the code
6
+ // that runs against your laptop is the code that runs in production.
7
+
8
+ import { createClient, type Client } from "@libsql/client";
9
+
10
+ const remote = process.env.TURSO_DATABASE_URL?.trim();
11
+
12
+ // A remote URL without a token fails at the first query with an auth error
13
+ // several layers from the cause. Better to say so while the reason is obvious.
14
+ if (remote && /^libsql:|^https:/i.test(remote) && !process.env.TURSO_AUTH_TOKEN?.trim()) {
15
+ throw new Error("TURSO_DATABASE_URL is set but TURSO_AUTH_TOKEN is not");
16
+ }
17
+
18
+ export const db: Client = createClient(
19
+ remote
20
+ ? { url: remote, authToken: process.env.TURSO_AUTH_TOKEN }
21
+ : { url: `file:${process.env.DB_PATH || "./data/app.db"}` },
22
+ );
23
+
24
+ /** Bring the schema up. Idempotent, so it is safe on every boot. */
25
+ export async function migrate(): Promise<void> {
26
+ await db.execute(`
27
+ CREATE TABLE IF NOT EXISTS visits (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ name TEXT NOT NULL,
30
+ path TEXT NOT NULL,
31
+ seen_at INTEGER NOT NULL
32
+ )
33
+ `);
34
+ await db.execute(`CREATE INDEX IF NOT EXISTS visits_name ON visits (name)`);
35
+ }
36
+
37
+ export async function recordVisit(name: string, path: string): Promise<number> {
38
+ await db.execute({
39
+ sql: `INSERT INTO visits (name, path, seen_at) VALUES (?, ?, ?)`,
40
+ args: [name, path, Date.now()],
41
+ });
42
+ const counted = await db.execute({
43
+ sql: `SELECT COUNT(*) AS n FROM visits WHERE name = ?`,
44
+ args: [name],
45
+ });
46
+ return Number(counted.rows[0]?.n ?? 0);
47
+ }
@@ -0,0 +1,44 @@
1
+ // The service behind Caddy.
2
+ //
3
+ // Bound to loopback on purpose. Caddy is the only client, and binding this to
4
+ // the public address would publish it on a port nothing virtual-hosts — which
5
+ // hands anyone scanning the box the app with the name stripped off the front.
6
+
7
+ import { migrate, recordVisit } from "./db";
8
+
9
+ const PORT = Number(process.env.PORT || 3000);
10
+ const HOSTNAME = process.env.HOST || "127.0.0.1";
11
+
12
+ await migrate();
13
+
14
+ const server = Bun.serve({
15
+ port: PORT,
16
+ hostname: HOSTNAME,
17
+
18
+ async fetch(req) {
19
+ const url = new URL(req.url);
20
+
21
+ // The Moshpit name arrives in a header and nowhere else. Nothing on this
22
+ // box resolved it — the visitor's resolver did, then connected straight
23
+ // here. `x-moshpit-name` is set when the request came through the
24
+ // pit.moshcode.sh gateway; `host` is what a direct visit carries.
25
+ const name = req.headers.get("x-moshpit-name") || req.headers.get("host") || "(no host header)";
26
+
27
+ if (url.pathname === "/health") {
28
+ return Response.json({ ok: true, name });
29
+ }
30
+
31
+ const visits = await recordVisit(name, url.pathname);
32
+ return new Response(
33
+ `${name} is served from this box.\n\npath: ${url.pathname}\nvisits: ${visits}\n`,
34
+ { headers: { "content-type": "text/plain; charset=utf-8" } },
35
+ );
36
+ },
37
+
38
+ error(err) {
39
+ console.error(err);
40
+ return new Response("internal error\n", { status: 500 });
41
+ },
42
+ });
43
+
44
+ console.log(`listening on ${server.hostname}:${server.port}`);
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "bun-caddy-sqlite",
3
+ "description": "Bun service + Caddy + SQLite (local file or Turso), published at a Moshpit name over IPv6",
4
+ "vars": {
5
+ "MOSHPIT_NAME": "the registered name to serve, e.g. foo.whatever",
6
+ "APP_ADDR": "where Bun listens, loopback only (default 127.0.0.1:3000)",
7
+ "TURSO_DATABASE_URL": "libsql:// URL for a hosted database; omit to use a local file",
8
+ "TURSO_AUTH_TOKEN": "token for the Turso database, required when the URL is remote"
9
+ }
10
+ }
@@ -0,0 +1,36 @@
1
+ # Caddy in front of a service already running on this box, published at a
2
+ # Moshpit name.
3
+ #
4
+ # The `http://` is required and is not a style choice. A Moshpit ending is not
5
+ # in the public DNS root, so no certificate authority will issue for it — leave
6
+ # the scheme off and Caddy will try to provision a certificate, fail, and never
7
+ # bring the site up. Everything served at a Moshpit name is plain HTTP.
8
+ #
9
+ # Nothing here resolves the name. The visitor's resolver did that; by the time a
10
+ # request arrives Caddy has only a Host header to match on, which is why the
11
+ # site address must be the name exactly as it is registered.
12
+
13
+ http://{$MOSHPIT_NAME:foo.whatever} {
14
+ reverse_proxy {$APP_ADDR:127.0.0.1:8080}
15
+
16
+ log {
17
+ output file /var/log/caddy/moshpit-service.log
18
+ }
19
+ }
20
+
21
+ # Every subdomain too. This block answers only once the name publishes a
22
+ # wildcard record — DNS Records tab in the Pit, the `*.` option with an AAAA
23
+ # at this box — because until then `anything.foo.whatever` resolves to nothing
24
+ # and no request reaches Caddy to match. Uncomment both together.
25
+ #
26
+ # The app sees which subdomain was asked for in the Host header, and Caddy
27
+ # matches exactly one label deep: `api.foo.whatever` answers,
28
+ # `a.b.foo.whatever` does not.
29
+ #
30
+ # http://*.{$MOSHPIT_NAME:foo.whatever} {
31
+ # reverse_proxy {$APP_ADDR:127.0.0.1:8080}
32
+ #
33
+ # log {
34
+ # output file /var/log/caddy/moshpit-service.log
35
+ # }
36
+ # }
@@ -0,0 +1,104 @@
1
+ # caddy-proxy
2
+
3
+ A name in front of something that is already running. No app, no database, no
4
+ runtime to keep alive — Caddy answers the Moshpit name and hands every request
5
+ to a local service on `127.0.0.1:8080` (or wherever `APP_ADDR` says).
6
+
7
+ This is the template for "I have a thing on this box, put a name on it": a
8
+ dev server, a dashboard, grafana, a game panel, anything that already listens
9
+ on loopback.
10
+
11
+ ## The part that surprises people
12
+
13
+ Three machines' worth of concerns, and they fail independently:
14
+
15
+ | | needs the resolver? | what it does |
16
+ |---|---|---|
17
+ | the box serving the name | **no** | Caddy matches a `Host` header, nothing more |
18
+ | the registry | — | holds the address the name points at |
19
+ | every visitor | **yes** | `sudo moshcode dns enable`, or the name resolves to nothing |
20
+
21
+ Nothing on the server ever resolves its own name. That is why there is no DNS
22
+ software in this template.
23
+
24
+ ## Deploying
25
+
26
+ 1. **Point the name at the box.** In the Pit, set `points at` to its public
27
+ IPv6 address — bare, no scheme, no brackets, no port:
28
+
29
+ ```sh
30
+ ip -6 addr show scope global | grep inet6
31
+ ```
32
+
33
+ Pick the globally routable one. An `fd..`/`fc..` address is unique-local
34
+ (Tailscale and friends live there) and the registry refuses it, because a
35
+ name pointed at one resolves somewhere only you can reach.
36
+
37
+ 2. **Serve it.** The service stays bound to loopback — Caddy is its only
38
+ client, and binding it publicly publishes it on a port nothing
39
+ virtual-hosts.
40
+
41
+ ```sh
42
+ export MOSHPIT_NAME=foo.whatever
43
+ export APP_ADDR=127.0.0.1:8080 # the default; change only if the service differs
44
+ sudo cp Caddyfile /etc/caddy/Caddyfile
45
+ sudo systemctl reload caddy
46
+ sudo ufw allow 80/tcp
47
+ ```
48
+
49
+ 3. **Reach it,** on any machine that should see the name:
50
+
51
+ ```sh
52
+ sudo moshcode dns enable
53
+ sudo cp deploy/moshcode-dns.service /etc/systemd/system/ # survives reboot
54
+ sudo systemctl enable --now moshcode-dns
55
+ ```
56
+
57
+ ## Every subdomain at once
58
+
59
+ One name covers one hostname. To answer `anything.foo.whatever` too — one
60
+ service per subdomain, or a wildcard tenant app — do both halves, in either
61
+ order, because neither works without the other:
62
+
63
+ 1. In the Pit's **DNS Records** tab, publish an **AAAA** record on the
64
+ `*.foo.whatever` option pointing at the same box. Until that exists the
65
+ subdomains resolve to nothing and no request ever reaches Caddy.
66
+ 2. Uncomment the wildcard block at the bottom of the Caddyfile and reload.
67
+ Caddy matches exactly one label deep, and the app reads which subdomain was
68
+ asked for from the `Host` header.
69
+
70
+ `foo.whatever` itself is not covered by a wildcard — keep the apex block (and
71
+ its own AAAA or `points at`) for that. This is how DNS wildcards work, not a
72
+ choice Caddy made.
73
+
74
+ ## Verifying, one layer at a time
75
+
76
+ A failure at any layer looks identical in a browser, so do not start there.
77
+
78
+ ```sh
79
+ # Server only — no DNS involved. Proves Caddy, the firewall, and the service.
80
+ curl -6 -H "Host: foo.whatever" http://[YOUR:V6:ADDR]/
81
+
82
+ # Resolver only. Proves the registry and the bridge.
83
+ moshcode dns resolve foo.whatever
84
+
85
+ # Both.
86
+ curl -6 http://foo.whatever/
87
+ ```
88
+
89
+ If the first works and the last does not, it is DNS. If the first fails, stop
90
+ looking at DNS.
91
+
92
+ ## Known limits
93
+
94
+ - **No HTTPS, ever.** No CA will issue for an ending outside the DNS root. That
95
+ rules out secure cookies, service workers, and WebCrypto in the browser. The
96
+ `http://` in the Caddyfile is what stops Caddy trying and failing.
97
+ - **Only machines running the resolver can reach the name.** Not phones, not a
98
+ colleague who has not installed it, not webhooks.
99
+ `pit.moshcode.sh/n/foo.whatever` is the URL for people who installed nothing.
100
+ - **Subdomains are opt-in.** `foo.whatever` works out of the box;
101
+ `www.foo.whatever` works only with the wildcard record described above.
102
+ - **Port 80 only** on the resolver path. A DNS record carries an address and
103
+ has nowhere to put a port, which is why Caddy listens on 80 and the
104
+ `host:port` part lives here, not in the registry.
@@ -0,0 +1,39 @@
1
+ # The Moshpit resolver, kept running across reboots.
2
+ #
3
+ # `moshcode dns enable` sets up two halves: a systemd-resolved drop-in that
4
+ # routes Moshpit endings at the bridge, and the bridge process itself. The
5
+ # drop-in is a file and survives a reboot on its own. The process does not —
6
+ # so after a restart the routing still points at a port with nothing behind it,
7
+ # and every Moshpit name stops resolving with no obvious cause. This unit is
8
+ # the missing half.
9
+ #
10
+ # sudo cp deploy/moshcode-dns.service /etc/systemd/system/
11
+ # sudo systemctl enable --now moshcode-dns
12
+ #
13
+ # Install this on machines that need to REACH Moshpit names. A box that only
14
+ # serves one does not need it — Caddy answers whatever Host header arrives and
15
+ # never resolves its own name.
16
+
17
+ [Unit]
18
+ Description=Moshpit DNS bridge
19
+ After=network-online.target
20
+ Wants=network-online.target
21
+ Before=systemd-resolved.service
22
+
23
+ [Service]
24
+ Type=simple
25
+ # Port 5354 is unprivileged, so this does not need root. The trade-off is that
26
+ # the parking responder cannot take port 80 and falls back to the public
27
+ # parking address — which only affects names that point nowhere yet.
28
+ ExecStart=/usr/bin/env moshcode dns start --port 5354
29
+ Restart=always
30
+ RestartSec=2
31
+
32
+ DynamicUser=yes
33
+ NoNewPrivileges=yes
34
+ PrivateTmp=yes
35
+ ProtectSystem=strict
36
+ ProtectHome=yes
37
+
38
+ [Install]
39
+ WantedBy=multi-user.target
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "caddy-proxy",
3
+ "description": "Caddy proxying a Moshpit name to a service already running on the box — no app, no database, just the name in front",
4
+ "vars": {
5
+ "MOSHPIT_NAME": "the registered name to serve, e.g. foo.whatever",
6
+ "APP_ADDR": "where the local service listens, loopback only (default 127.0.0.1:8080)"
7
+ }
8
+ }
@@ -0,0 +1,16 @@
1
+ # Caddy serving files at a Moshpit name.
2
+ #
3
+ # The `http://` is required and is not a style choice. A Moshpit ending is not
4
+ # in the public DNS root, so no certificate authority will issue for it — leave
5
+ # the scheme off and Caddy will try to provision a certificate, fail, and never
6
+ # bring the site up. Everything served at a Moshpit name is plain HTTP.
7
+
8
+ http://{$MOSHPIT_NAME:foo.whatever} {
9
+ root * {$SITE_ROOT:/srv/moshpit-site}
10
+ file_server
11
+ encode gzip
12
+
13
+ log {
14
+ output file /var/log/caddy/moshpit-site.log
15
+ }
16
+ }