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

An app's undeclared hosts, the list to copy into forge.json, speak the home page's language #9992

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9971-an-app-s-undeclared-hosts-the-list-to-co → main 4 commits fork 2 threads · 2 unresolved

  • Merged. scout/9971-an-app-s-undeclared-hosts-the-list-to-co was merged into main as f7dde71.

Review

Sign in to review this change.
3 files changed +122 −15
src/web/studio/egress.ts +20 −12
@@ -1,12 +1,14 @@
11 /**
22 * The hosts an app's code reached without declaring them, said to the app's
33 * writers while egress is only reported (`apps/egress_report.ts`): the list
4 * to copy into `forge.json` before the forge starts refusing them.
4 * to copy into `forge.json` before the forge starts refusing them. A block
5 * under a heading, one row per host, in the home page's language (#9971).
56 */
67
78 import { egressModeSentence, undeclaredHosts } from "../../apps/egress_report.ts";
89 import { egressMode } from "../../apps/contain.ts";
910 import { html, type Html } from "../html.ts";
11 import { row } from "../ui.ts";
1012
1113 export function egressNote(
1214 dataDir: string,
@@ -16,20 +18,26 @@ export function egressNote(
1618 /** The forge's egress mode now (`core/capacity.ts`). */
1719 mode: "enforce" | "report" = egressMode(),
1820 ): Html | null {
21 // Once egress is enforced the forge refuses these calls; the note is for the
outdated

Box 2 (principle 1, one confident sentence): return null when mode is "enforce", as the issue asks. Then egressModeSentence need not carry the enforce wording here.

22 // window before that, when adding them to forge.json still saves the app.
23 if (mode === "enforce") return null;
1924 const { reached, entries } = undeclaredHosts(dataDir, repoId, declared);
2025 if (reached.length === 0) return null;
21 return html`<div class="card" id="app-egress">
26 return html`<section class="settings-block" id="app-egress">
27 <h2>Reached, not declared</h2>
2228 <p class="small">
23 This app reached
24 ${reached.map(
25 (r, i) =>
26 html`${i === 0 ? "" : ", "}<code>${r.host}:${r.port}</code>
27 <span class="muted">(${r.count}×)</span>`,
28 )},
29 which <code>forge.json</code> does not declare.
30 ${egressModeSentence(mode)}
31 Add them to <code>egress</code>:
29 This app reached these hosts, which <code>forge.json</code> does not declare.
30 ${egressModeSentence("report")}
3231 </p>
32 <ul class="list setting-list">
33 ${reached.map((r) =>
34 row({
35 title: html`<code>${r.host}:${r.port}</code>`,
36 meta: html`${r.count}× · last ${r.last.slice(0, 10)}`,
37 }),
38 )}
39 </ul>
40 <p class="small">Add them to <code>egress</code> in <code>forge.json</code>:</p>
3341 <pre><code>"egress": ${JSON.stringify(entries)}</code></pre>
34 </div>`;
42 </section>`;
3543 }
test/app-egress-language.test.ts added +99
@@ -0,0 +1,99 @@
1 /**
2 * The hosts an app's code reached undeclared speak the home page's language
3 * (#9971): a settings-block under an h2, one row per host, the forge.json
4 * line to copy, no card; for the app's writers only, and each host escaped.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { createRepo } from "../src/actions/repos.ts";
12 import { noteReached } from "../src/apps/egress_report.ts";
13 import { createApp as createAppRecord } from "../src/core/apps.ts";
14 import { setCapacity } from "../src/core/capacity.ts";
15 import { createSession } from "../src/core/sessions.ts";
16 import { createUser, getUserByName } from "../src/core/users.ts";
17 import type { Repo } from "../src/core/repos.ts";
18 import { serve, type Servers } from "../src/http/server.ts";
19 import { egressNote } from "../src/web/studio/egress.ts";
20 import { seedAdmin } from "./helpers/admin.ts";
21 import { quietLogs } from "./helpers/globals.ts";
22
23 quietLogs();
24
25 let dataDir = "";
26 let app: App, servers: Servers, repo: Repo;
27 let writer = "", reader = "";
28
29 beforeAll(async () => {
30 dataDir = await mkdtemp(join(tmpdir(), "forge-app-egress-lang-"));
31 app = createApp({ dataDir, port: 0 });
32 servers = await serve(app);
33 const a = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
34 if (!a.ok) throw new Error(a.error);
35 const b = await createUser(app.db, { origin: "seed", trusted: true, username: "bobby", email: "[email protected]", password: "correct-horse-battery" });
36 if (!b.ok) throw new Error(b.error);
37 const r = await createRepo(app, a.value, { name: "notes", visibility: "public" });
38 if (!r.ok) throw new Error("repo failed");
39 repo = r.value;
40 const made = createAppRecord(app.db, { repoId: repo.id, slug: "notes", access: "public" });
41 if (!made.ok) throw new Error(made.error);
42 setCapacity(app.db, "cells.egress", "report", a.value.id);
43 noteReached(dataDir, repo.id, "api.example.com", 443);
44 noteReached(dataDir, repo.id, "api.example.com", 443);
45 noteReached(dataDir, repo.id, "db.example.com", 5432);
46 writer = `forge_session=${createSession(app.db, getUserByName(app.db, "alice")!.id).token}`;
47 reader = `forge_session=${createSession(app.db, getUserByName(app.db, "bobby")!.id).token}`;
48 });
49 afterAll(async () => {
50 servers.stop();
51 app.close();
52 await rm(dataDir, { recursive: true, force: true });
53 });
54
55 const page = async (cookie: string | null) =>
56 (await fetch(`${servers.url}/alice/notes/app`, cookie ? { headers: { cookie } } : {})).text();
57 const block = (p: string) => {
58 const at = p.indexOf('id="app-egress"');
59 return at < 0 ? "" : p.slice(p.lastIndexOf("<", at), p.indexOf("</section>", at));
60 };
61
62 test("a writer sees a settings-block under an h2, one row per host, the line to copy, no card", async () => {
63 const b = block(await page(writer));
64 expect(b).toMatch(/^<section class="settings-block" id="app-egress">\s*<h2>[^<]+<\/h2>/);
65 expect(b).not.toContain('class="card');
66 expect((b.match(/class="row"/g) ?? []).length).toBe(2);
67 expect(b).toContain("<code>api.example.com:443</code>");
68 expect(b).toContain("<code>db.example.com:5432</code>");
69 expect(b).toContain("2×");
70 expect(b).toContain("once it is enforced they are refused");
71 expect(b).toContain('"egress": [' + "&quot;db.example.com:5432&quot;,&quot;api.example.com&quot;]");
72 });
73
74 test("a reader and a stranger see nothing of it", async () => {
75 expect(await page(reader)).not.toContain('id="app-egress"');
76 expect(await page(null)).not.toContain('id="app-egress"');
77 });
78

This test pins the behaviour the issue rules out. Replace it with one proving egressNote(..., "enforce") is null.

79 test("once egress is enforced the note is gone, on the page too", async () => {
80 expect(egressNote(dataDir, repo.id, [], "enforce")).toBeNull();
81 setCapacity(app.db, "cells.egress", "enforce", getUserByName(app.db, "alice")!.id);
82 try {
83 expect(await page(writer)).not.toContain('id="app-egress"');
84 } finally {
85 setCapacity(app.db, "cells.egress", "report", getUserByName(app.db, "alice")!.id);
86 }
87 });
88
89 test("nothing reached, or all of it declared since, draws nothing", () => {
90 expect(egressNote(dataDir, "0nothing0", [], "report")).toBeNull();
91 expect(egressNote(dataDir, repo.id, ["api.example.com", "db.example.com:5432"], "report")).toBeNull();
92 });
93
94 test("a host is text, never markup", () => {
95 noteReached(dataDir, "0hostile0", "<img src=x>.example.com", 443);
96 const b = egressNote(dataDir, "0hostile0", [], "report")!.text;
97 expect(b).not.toContain("<img");
98 expect(b).toContain("&lt;img src=x&gt;.example.com:443");
99 });
test/contain.test.ts +3 −3
@@ -354,14 +354,14 @@ describe("egress, reported: what an app reached is kept per repository and shown
354354 ["api.stripe.com", 443, 2],
355355 ]);
356356 expect(reachedHosts(dataDir, "01other")).toEqual([]);
357 const note = String(egressNote(dataDir, "01repo", []));
357 const note = String(egressNote(dataDir, "01repo", [], "report"));
358358 expect(note).toContain("api.stripe.com:443");
359359 expect(note).toContain("&quot;api.stripe.com&quot;");
360360 expect(note).toContain("db.example.com:5432");
361 const after = String(egressNote(dataDir, "01repo", ["api.stripe.com"]));
361 const after = String(egressNote(dataDir, "01repo", ["api.stripe.com"], "report"));
362362 expect(after).not.toContain("api.stripe.com");
363363 expect(
364 egressNote(dataDir, "01repo", ["api.stripe.com", "db.example.com:5432"]),
364 egressNote(dataDir, "01repo", ["api.stripe.com", "db.example.com:5432"], "report"),
365365 ).toBeNull();
366366 });
367367 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs