Public: anyone can read this; the crew can change it.

The host tunnel config lists each app's own domain #11873

opened this

Check · Review · Land · Live — Its check is queued. Next: reviewer, no review yet.

Ready to land

scout/forge:scout/11871-the-host-tunnel-config-lists-each-app-s → main 1 commit fork

Review

Sign in to review this change.
4 files changed +91 −0
infra/forge-box/cloudflared-forge.yml added +15
@@ -0,0 +1,15 @@
1 # Generated by `bun scripts/tunnel-config.ts`; do not edit by hand.
2 # The tunnel name and credentials-file are set on the box, not here.
3 ingress:
4 - hostname: "forge.open-platform.sh"
5 service: http://127.0.0.1:3000
6 - hostname: "*--forge.open-platform.sh"
7 service: http://127.0.0.1:3000
8 - hostname: "*--forge-trevato-apps.open-platform.sh"
9 service: http://127.0.0.1:3000
10 # one rule per app's own domain (app_domain)
11 - hostname: "game.cheffryjames.com"
12 service: http://127.0.0.1:3000
13 - hostname: "www.cheffryjames.com"
14 service: http://127.0.0.1:3000
15 - service: http_status:404
scripts/tunnel-config.ts added +20
@@ -0,0 +1,20 @@
1 /**
2 * `bun scripts/tunnel-config.ts [--db <forge.db>] [--write]` — renders the host
3 * tunnel's ingress from the apps' own domains. Prints it, or with `--write`
4 * replaces infra/forge-box/cloudflared-forge.yml. Reads the database only.
5 */
6 import { Database } from "bun:sqlite";
7 import { writeFileSync } from "node:fs";
8 import { join } from "node:path";
9 import { appDomainHosts, renderTunnelConfig } from "../src/system/tunnel_config.ts";
10
11 const args = process.argv.slice(2);
12 const i = args.indexOf("--db");
13 const dbPath = i >= 0 ? args[i + 1] : join(process.env.FORGE_DATA ?? "", "forge.db");
14 if (!dbPath || dbPath === "forge.db" && !process.env.FORGE_DATA) {
15 console.error("usage: bun scripts/tunnel-config.ts --db <forge.db> [--write]");
16 process.exit(2);
17 }
18 const text = renderTunnelConfig(appDomainHosts(new Database(dbPath, { readonly: true })));
19 if (args.includes("--write")) writeFileSync(join(import.meta.dir, "../infra/forge-box/cloudflared-forge.yml"), text);
20 else process.stdout.write(text);
src/system/tunnel_config.ts added +40
@@ -0,0 +1,40 @@
1 /**
2 * The host tunnel's ingress: the forge's three hostnames, then one rule per
3 * app domain in the `app_domain` table, then a 404 catch-all. Plain data in
4 * (hostnames), plain text out (cloudflared's yml). `scripts/tunnel-config.ts`
5 * reads the table and writes `infra/forge-box/cloudflared-forge.yml`.
6 */
7 import type { Database } from "bun:sqlite";
8
9 export const FORGE_ORIGIN = "http://127.0.0.1:3000";
10 export const FORGE_HOSTNAMES = [
11 "forge.open-platform.sh",
12 "*--forge.open-platform.sh",
13 "*--forge-trevato-apps.open-platform.sh",
14 ] as const;
15
16 /** Every app's own domain, verified or not (the forge refuses what it cannot prove), sorted. */
17 export function appDomainHosts(db: Database): string[] {
18 return db
19 .query<{ host: string }, []>("SELECT host FROM app_domain ORDER BY host")
20 .all()
21 .map((r) => r.host);
22 }
23
24 const HOST = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
25
26 export function renderTunnelConfig(appHosts: readonly string[]): string {
27 const hosts = [...new Set(appHosts)].sort();
28 for (const h of hosts) if (!HOST.test(h)) throw new Error(`not a hostname: ${h}`);
29 const rule = (h: string) => ` - hostname: "${h}"\n service: ${FORGE_ORIGIN}`;
30 return [
31 "# Generated by `bun scripts/tunnel-config.ts`; do not edit by hand.",
32 "# The tunnel name and credentials-file are set on the box, not here.",
33 "ingress:",
34 ...FORGE_HOSTNAMES.map(rule),
35 " # one rule per app's own domain (app_domain)",
36 ...hosts.map(rule),
37 " - service: http_status:404",
38 "",
39 ].join("\n");
40 }
test/forge-box-unit.test.ts +16
@@ -49,3 +49,19 @@ test("the install script places the unit, keeps no secret, and is executable", (
4949 expect(sh).not.toMatch(/PASSPHRASE=\S/);
5050 expect(require("node:fs").statSync(path).mode & 0o111).toBeGreaterThan(0);
5151 });
52
53 test("the tunnel config has a rule for every app domain, and the file is what the generator renders", async () => {
54 const { Database } = await import("bun:sqlite");
55 const { appDomainHosts, renderTunnelConfig, FORGE_HOSTNAMES } = await import("../src/system/tunnel_config.ts");
56 const db = new Database(":memory:");
57 db.run("CREATE TABLE app_domain (host TEXT PRIMARY KEY, verified_at TEXT)");
58 for (const h of ["www.cheffryjames.com", "game.cheffryjames.com", "pending.example.org"])
59 db.run("INSERT INTO app_domain (host) VALUES (?)", [h]);
60 const text = renderTunnelConfig(appDomainHosts(db));
61 for (const h of [...FORGE_HOSTNAMES, ...appDomainHosts(db)]) expect(text).toContain(`- hostname: "${h}"\n service: http://127.0.0.1:3000`);
62 expect(text.trimEnd().endsWith("- service: http_status:404")).toBe(true);
63 expect(() => renderTunnelConfig(["bad host\nservice: x"])).toThrow();
64 // the committed file carries the domains the forge serves today
65 const committed = readFileSync(join(root, "infra/forge-box/cloudflared-forge.yml"), "utf8");
66 expect(committed).toBe(renderTunnelConfig(["game.cheffryjames.com", "www.cheffryjames.com"]));
67 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs