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.
- package/LICENSE +21 -0
- package/README.md +580 -0
- package/bin/moshcode.mjs +674 -0
- package/bin/moshscript.mjs +29 -0
- package/examples/alive.mosh +6 -0
- package/examples/scripting-the-cli.mosh +21 -0
- package/examples/team-secrets.mosh +20 -0
- package/examples/templates/bun-caddy-sqlite/.env.example +14 -0
- package/examples/templates/bun-caddy-sqlite/Caddyfile +18 -0
- package/examples/templates/bun-caddy-sqlite/README.md +97 -0
- package/examples/templates/bun-caddy-sqlite/deploy/moshcode-dns.service +39 -0
- package/examples/templates/bun-caddy-sqlite/deploy/moshpit-service.service +38 -0
- package/examples/templates/bun-caddy-sqlite/package.json +15 -0
- package/examples/templates/bun-caddy-sqlite/src/db.ts +47 -0
- package/examples/templates/bun-caddy-sqlite/src/server.ts +44 -0
- package/examples/templates/bun-caddy-sqlite/template.json +10 -0
- package/examples/templates/caddy-proxy/Caddyfile +36 -0
- package/examples/templates/caddy-proxy/README.md +104 -0
- package/examples/templates/caddy-proxy/deploy/moshcode-dns.service +39 -0
- package/examples/templates/caddy-proxy/template.json +8 -0
- package/examples/templates/caddy-static/Caddyfile +16 -0
- package/examples/templates/caddy-static/README.md +90 -0
- package/examples/templates/caddy-static/deploy/moshcode-dns.service +39 -0
- package/examples/templates/caddy-static/site/index.html +11 -0
- package/examples/templates/caddy-static/template.json +8 -0
- package/install.sh +194 -0
- package/package.json +28 -0
- package/prd/0000-template.md +49 -0
- package/prd/0001-wrap-ugig-and-coinpay-clis.md +121 -0
- package/prd/0002-separate-agent-and-raw-engine-launches.md +113 -0
- package/prd/0003-cross-engine-mcp-and-skill-installation.md +165 -0
- package/prd/0004-moshscript-run-programmable-moshcode.md +344 -0
- package/prd/0005-hosted-moshpit-resolver.md +192 -0
- package/prd/0006-help.md +359 -0
- package/prd/0007-profullstack-site-init.md +1183 -0
- package/prd/README.md +26 -0
- package/src/ads.mjs +58 -0
- package/src/auth.mjs +193 -0
- package/src/cli-schema.mjs +533 -0
- package/src/cli.mjs +118 -0
- package/src/commands.mjs +259 -0
- package/src/completion.mjs +594 -0
- package/src/console.mjs +244 -0
- package/src/dns-system.mjs +404 -0
- package/src/dns.mjs +2872 -0
- package/src/doh-server.mjs +256 -0
- package/src/doh.mjs +218 -0
- package/src/engines.mjs +385 -0
- package/src/escalate.mjs +85 -0
- package/src/help.mjs +443 -0
- package/src/integrations.mjs +265 -0
- package/src/mcp-catalog.mjs +50 -0
- package/src/mcp.mjs +155 -0
- package/src/mirror.mjs +187 -0
- package/src/notify.mjs +86 -0
- package/src/open-url.mjs +34 -0
- package/src/parking-http.mjs +65 -0
- package/src/pins.mjs +190 -0
- package/src/pit-url.mjs +13 -0
- package/src/prd.mjs +341 -0
- package/src/pty.mjs +176 -0
- package/src/pwd.mjs +103 -0
- package/src/registry.mjs +37 -0
- package/src/release-install.mjs +191 -0
- package/src/runtime.mjs +161 -0
- package/src/selfupdate.mjs +215 -0
- package/src/serve.mjs +502 -0
- package/src/skills.mjs +93 -0
- package/src/tabs.mjs +144 -0
- package/src/templates.mjs +456 -0
- package/src/tools.mjs +231 -0
- package/src/trade.mjs +137 -0
- package/src/trust.mjs +712 -0
- package/src/tui.mjs +736 -0
- package/src/ui.mjs +49 -0
- package/src/uninstall.mjs +113 -0
- 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,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
|
+
}
|