Public: anyone can read this; the crew can change it.
An app host's /__forge/ door shows the exact ssh command for the app's terminal to its writers, or says the terminal is off #5969
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5957-an-app-host-s-forge-door-shows-the-exact → main 3 commits fork 2 threads · 2 unresolved
3 files
changed +75
−2
src/apps/door.ts +13 −2
| @@ -14,10 +14,15 @@ export type DoorFacts = { | ||
| 14 | 14 | tell?: string; |
| 15 | 15 | /** This host's own sign-in door, offered when nobody is signed in. */ |
| 16 | 16 | signin?: string; |
| 17 | /** | |
| 18 | * The terminal, only for someone who may write the repository: the exact | |
| 19 | * `ssh …` command, or null when the terminal is off. Unset shows nothing. | |
| 20 | */ | |
| 21 | ssh?: string | null; | |
| 17 | 22 | }; |
| 18 | 23 | |
| 19 | 24 | const STYLE = |
| 20 | "body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}li{margin:.4rem 0}"; | |
| 25 | "body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}li{margin:.4rem 0}pre{overflow-x:auto;padding:.6rem;background:#eee;color:#111;user-select:all}"; | |
| 21 | 26 | const STYLE_HASH = createHash("sha256").update(STYLE).digest("base64"); |
| 22 | 27 | |
| 23 | 28 | /** The same words as the pill's accessible name. */ |
| @@ -42,9 +47,15 @@ export function doorPage(facts: DoorFacts, head: boolean): Response { | ||
| 42 | 47 | if (facts.tell !== undefined) links.push(link(facts.tell, "Tell the crew")); |
| 43 | 48 | else if (facts.signin !== undefined) |
| 44 | 49 | links.push(link(facts.signin, "Sign in to tell the crew")); |
| 50 | const terminal = | |
| 51 | facts.ssh === undefined | |
| 52 | ? "" | |
| 53 | : facts.ssh === null | |
| 54 | ? "<p>The terminal is off.</p>" | |
| 55 | : `<p>Terminal:</p><pre><code>${escapeAttr(facts.ssh)}</code></pre>`; | |
| 45 | 56 | const html = `<!doctype html> |
| 46 | 57 | <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>${escapeAttr(where.app)}, ${escapeAttr(doorWhere(where))}</title><style>${STYLE}</style></head> |
| 47 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${escapeAttr(doorWhere(where))}</p><ul>${links.join("")}</ul></main></body></html> | |
| 58 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${escapeAttr(doorWhere(where))}</p><ul>${links.join("")}</ul>${terminal}</main></body></html> | |
| 48 | 59 | `; |
| 49 | 60 | return new Response(head ? null : html, { |
| 50 | 61 | headers: { |
src/apps/proxy.ts +23
| @@ -26,6 +26,7 @@ import { getAppById, getAppBySlug, type AppRecord } from "../core/apps.ts"; | ||
| 26 | 26 | import { getVerifiedDomain } from "../core/domains.ts"; |
| 27 | 27 | import { |
| 28 | 28 | canEnterApp, |
| 29 | canWrite, | |
| 29 | 30 | canRead, |
| 30 | 31 | getRepoById, |
| 31 | 32 | ownerInStanding, |
| @@ -414,6 +415,26 @@ export function pillWhere( | ||
| 414 | 415 | return where; |
| 415 | 416 | } |
| 416 | 417 | |
| 418 | /** | |
| 419 | * What the door says of the terminal: for a viewer who may write the | |
| 420 | * repository, the exact command (the preview's login on a preview), or null | |
| 421 | * when the terminal is off; for anyone else, nothing. | |
| 422 | */ | |
| 423 | function doorSsh( | |
outdated
A never-live app has no status, so this falls through to sshFor and may show a command for an app with no terminal. Prefer the manifest tty declaration here, and test it. |
||
| 424 | runtime: AppsRuntime, | |
| 425 | target: AppTarget, | |
| 426 | who: GrantWho | undefined, | |
| 427 | ): string | null | undefined { | |
| 428 | if (who === undefined) return undefined; | |
| 429 | const db = runtime.app.db; | |
| 430 | const viewer = getUserById(db, who.id); | |
| 431 | const repo = getRepoById(db, target.app.repoId); | |
| 432 | if (viewer === undefined || !isActive(viewer) || repo === undefined) return undefined; | |
| 433 | if (!canWrite(db, repo, viewer)) return undefined; | |
| 434 | if (runtime.envStatus(target.app, target.env)?.tty === null) return null; | |
| 435 | return runtime.sshFor(target.app, target.env) ?? null; | |
| 436 | } | |
| 437 | ||
| 417 | 438 | /** Whether the person a grant names may enter this app now. */ |
| 418 | 439 | function stillEntitled( |
| 419 | 440 | runtime: AppsRuntime, |
| @@ -656,6 +677,7 @@ export async function proxyApp( | ||
| 656 | 677 | signed?.who?.id, |
| 657 | 678 | ), |
| 658 | 679 | ); |
| 680 | const ssh = doorSsh(runtime, target, signed?.who); | |
| 659 | 681 | const where = timePhase(request, "pull", () => |
| 660 | 682 | pillWhere(runtime, target, signed?.who), |
| 661 | 683 | ); |
| @@ -664,6 +686,7 @@ export async function proxyApp( | ||
| 664 | 686 | { |
| 665 | 687 | where, |
| 666 | 688 | forgeUrl: runtime.options.forgeUrl, |
| 689 | ...(ssh === undefined ? {} : { ssh }), | |
| 667 | 690 | ...(tell === undefined ? { signin: "/__forge/signin" } : { tell }), |
| 668 | 691 | }, |
| 669 | 692 | request.method === "HEAD", |
test/apps-door.test.ts +39
| @@ -178,6 +178,45 @@ test("HEAD answers the headers with no body; POST is a 405 that says what is all | ||
| 178 | 178 | expect(post.headers.get("x-content-type-options")).toBe("nosniff"); |
| 179 | 179 | }); |
| 180 | 180 | |
| 181 | test("the terminal: a writer sees the exact command, a reader and a visitor see nothing", async () => { | |
| 182 | const cfg = app.config as { sshPort?: number | undefined }; | |
| 183 | const was = cfg.sshPort; | |
| 184 | const b = await createUser(app.db, { | |
| 185 | origin: "seed", | |
| 186 | trusted: true, | |
| 187 | username: "carol", | |
| 188 | email: "[email protected]", | |
| 189 | password: "a-good-passphrase", | |
| 190 | }); | |
| 191 | if (!b.ok) throw new Error(b.error); | |
| 192 | const cookieFor = (env: string, id: string, name: string, role: "admin" | "user") => | |
| 193 | `forge_grant_${record.id.slice(-8)}=${signGrant(dataDir, { app: record, env } as AppTarget, 60_000, Date.now(), { id, name, role })}`; | |
| 194 | try { | |
| 195 | cfg.sshPort = 2222; | |
| 196 | const prod = await ( | |
| 197 | await door(`notes.${DOMAIN}`, cookieFor("production", alice, "alice", "admin")) | |
| 198 | ).text(); | |
| 199 | expect(prod).toContain("<code>ssh -p 2222 [email protected]</code>"); | |
| 200 | const prev = await ( | |
| 201 | await door(`notes--${pull}.${DOMAIN}`, cookieFor(`pull-${pull}`, alice, "alice", "admin")) | |
| 202 | ).text(); | |
| 203 | expect(prev).toContain(`<code>ssh -p 2222 notes--${pull}@forge.test</code>`); | |
| 204 | for (const cookie of [undefined, cookieFor("production", b.value.id, "carol", "user")]) { | |
| 205 | const html = await (await door(`notes.${DOMAIN}`, cookie)).text(); | |
| 206 | expect(html).not.toContain("ssh"); | |
| 207 | expect(html).not.toContain("terminal is off"); | |
| 208 | } | |
| 209 | cfg.sshPort = undefined; | |
| 210 | const off = await ( | |
| 211 | await door(`notes.${DOMAIN}`, cookieFor("production", alice, "alice", "admin")) | |
| 212 | ).text(); | |
| 213 | expect(off).toContain("The terminal is off."); | |
| 214 | expect(off).not.toContain("ssh"); | |
| 215 | } finally { | |
| 216 | cfg.sshPort = was; | |
| 217 | } | |
| 218 | }); | |
| 219 | ||
| 181 | 220 | test("every name the door draws is escaped, for production and a preview", () => { |
| 182 | 221 | const evil = `<script>alert("x")</script>'`; |
| 183 | 222 | const out = (where: PillWhere) => |
new above
offline
Add tabindex="0" to this pre: with overflow-x:auto it is a scroll container, and a keyboard user cannot reach it otherwise.