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

An app's egress grants speak the home page's language, and say plainly that everything else is refused #9858

Merged opened this

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

Merged

scout/forge:scout/9856-an-app-s-egress-grants-speak-the-home-pa → main 4 commits fork

  • Merged. scout/9856-an-app-s-egress-grants-speak-the-home-pa was merged into main as 6db882e.

Review

Sign in to review this change.
3 files changed +52 −25
docs/design/areas/settings.md +2
@@ -130,6 +130,8 @@ Settings › Prompts (`promptsView()` in `src/web/pages/prompts.ts`, #9825) adds
130130
131131 Settings › Hooks (`hooksPage()` in `src/web/pages/hooks.ts`, #9762) adds no component class and has no card or table: Webhooks and Add a webhook are each a `<section class="settings-block">` with an `<h2>`. Each hook is a `row()` in `<ul class="list setting-list">` (URL as title; events, state, added date and "unsigned" as meta; Send ping and a confirmed Delete in the trail), with its recent deliveries as nested child rows, Redeliver in the trail of a failed one. With no hook, one plain sentence. The secret is never drawn. Pinned by `test/hooks-rows.test.ts`.
132132
133 Settings › App › Private destinations asked for (`appEgressView()` in `src/web/pages/settings.ts`, #9856) adds no class and has no card: a `<section class="settings-block" id="egress">` with its `<h2>`, one sentence that every other host is refused, each destination a `row()` in `<ul class="list setting-list">` with Grant or a confirmed Revoke in the trail, and the recent changes as a timeline. With nothing asked for, one plain line: the app reaches only the forge. Pinned by `test/app-egress-grants.test.ts`.
134
133135 Settings › Model (`modelPage()` in `src/web/pages/model.ts`, #9800) adds no class and has no card: Your model budget is one `<section class="settings-block">` with an `<h2>`. A grant an admin gave is one `row()` in `<ul class="list setting-list" id="model-budget">`; with nothing (or an own subscription, or an admin) one plain sentence says what the account may spend and how to add a subscription. The token form sits straight in the block, typed once and never shown again. `ceilingCard` and `grantCard` stay cards on the account's page. Pinned by `test/model-page-shape.test.ts`.
134136
135137 ## Organizations
src/web/pages/settings.ts +21 −25
@@ -763,7 +763,7 @@ function appSettingsView(
763763 <div class="mt">
764764 ${appDomainsView(base, domains.ok ? domains.value : undefined)}
765765 </div>
766 ${when(egress !== undefined && egress.rows.length > 0, () => html`<div class="mt">${appEgressView(base, egress!)}</div>`)}
766 ${when(egress !== undefined, () => html`<div class="mt">${appEgressView(base, egress!)}</div>`)}
767767 <div class="mt">
768768 ${card(
769769 html`<p>
@@ -1371,30 +1371,26 @@ function appEgressView(base: string, egress: EgressView): Html {
13711371 >`,
13721372 }),
13731373 );
1374 return card(
1375 html`<div class="card-body">
1376 <p class="muted small">
1377 Each is an exact address and port forge.json asks for. A grant opens
1378 that one destination for the app's cells and nothing else.${egress.canChange
1379 ? null
1380 : " Only a site admin grants or revokes."}
1381 </p>
1382 </div>
1383 <ul class="list setting-list">
1384 ${rows}
1385 </ul>
1386 ${when(
1387 changes.length > 0,
1388 () =>
1389 html`<div class="card-body">
1390 <div class="timeline">${changes}</div>
1391 </div>`,
1392 )}`,
1393 {
1394 head: html`${icon("globe")}
1395 <h2>Private destinations asked for</h2>`,
1396 },
1397 );
1374 return html`<section class="settings-block" id="egress">
1375 <h2>Private destinations asked for</h2>
1376 ${rows.length === 0
1377 ? html`<p class="muted small">
1378 This app reaches only the forge; every other host is refused.
1379 </p>`
1380 : html`<p class="muted small">
1381 Each is an exact address and port forge.json asks for; every other
1382 host is refused.${egress.canChange
1383 ? null
1384 : " Only a site admin grants or revokes."}
1385 </p>
1386 <ul class="list setting-list">
1387 ${rows}
1388 </ul>`}
1389 ${when(
1390 changes.length > 0,
1391 () => html`<div class="timeline">${changes}</div>`,
1392 )}
1393 </section>`;
13981394 }
13991395
14001396 const EGRESS_ERRORS: Record<string, string> = {
test/app-egress-grants.test.ts +29
@@ -235,3 +235,32 @@ describe("private destinations asked for", () => {
235235 expect(verbs).toContain(`revoked the app's grant on ${ARM}`);
236236 });
237237 });
238
239 describe("the egress section speaks the home page's language", () => {
240 const section = async (cookie: string, path: string) => {
241 const page = await (await web(cookie, path)).text();
242 const from = page.indexOf('<section class="settings-block" id="egress">');
243 expect(from).toBeGreaterThan(-1);
244 return words(page.slice(from, page.indexOf("</section>", from)));
245 };
246
247 test("with destinations: a heading, the refusal sentence and rows, no card", async () => {
248 const s = await section(adaCookie, "/cy/arm/settings/app");
249 expect(s).toContain("<h2>Private destinations asked for</h2>");
250 expect(s).toContain("every other host is refused");
251 expect(s).not.toContain('class="card');
252 expect(s).toContain(`/settings/app/egress/${encodeURIComponent(ARM)}/grant`);
253 });
254
255 test("with none asked for: one plain line, no list, no card", async () => {
256 const made = await createRepo(app, cy, { name: "plain", visibility: "public", description: "" });
257 if (!made.ok) throw new Error(String(made.error));
258 const row = insertApp(app.db, { repoId: made.value.id, slug: "plain", access: "public" });
259 if (!row.ok) throw new Error(row.error);
260 const s = await section(adaCookie, "/cy/plain/settings/app");
261 expect(s).toContain("This app reaches only the forge; every other host is refused.");
262 expect(s).not.toContain('class="card');
263 expect(s).not.toContain("setting-list");
264 expect(s).not.toContain('data-tone="danger"');
265 });
266 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs