Public: anyone can read this; the crew can change it.
A preview's /__forge/ door links the pull it previews, with its title #6052
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6046-a-preview-s-forge-door-links-the-pull-it → main 3 commits fork 1 thread · 1 unresolved
2 files
changed +50
−2
src/apps/door.ts +11 −1
| @@ -47,6 +47,16 @@ export function doorWhere(where: PillWhere): string { | ||
| 47 | 47 | const link = (href: string, text: string): string => |
| 48 | 48 | `<li><a href="${escapeAttr(href)}">${escapeAttr(text)}</a></li>`; |
| 49 | 49 | |
| 50 | /** The where line; a reader of the pull gets its number linked and its title. */ | |
| 51 | function whereLine(where: PillWhere, forgeUrl: string): string { | |
| 52 | const words = escapeAttr(doorWhere(where)); | |
| 53 | if (where.pull === undefined || where.repo === undefined || where.title === undefined) | |
| 54 | return words; | |
| 55 | const href = new URL(`/${where.repo}/pulls/${where.pull}`, forgeUrl).href; | |
| 56 | const n = `pull #${where.pull}`; | |
| 57 | return `${words.replace(n, `<a href="${escapeAttr(href)}">${n}</a>`)}: ${escapeAttr(where.title)}`; | |
| 58 | } | |
| 59 | ||
| 50 | 60 | /** `2026-10-25 14:03 UTC`, or the raw text when it is not a date. */ |
| 51 | 61 | function when(at: string): string { |
| 52 | 62 | const t = Date.parse(at); |
| @@ -109,7 +119,7 @@ export function doorPage(facts: DoorFacts, head: boolean): Response { | ||
| 109 | 119 | : `<p>Terminal:</p><pre><code>${escapeAttr(facts.ssh)}</code></pre>`; |
| 110 | 120 | const html = `<!doctype html> |
| 111 | 121 | <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> |
| 112 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${escapeAttr(doorWhere(where))}</p>${servingHtml(facts)}${pullsHtml(facts.pulls)}<ul>${links.join("")}</ul>${terminal}</main></body></html> | |
| 122 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${whereLine(where, forgeUrl)}</p>${servingHtml(facts)}${pullsHtml(facts.pulls)}<ul>${links.join("")}</ul>${terminal}</main></body></html> | |
| 113 | 123 | `; |
| 114 | 124 | return new Response(head ? null : html, { |
| 115 | 125 | headers: { |
test/apps-door.test.ts +39 −1
| @@ -104,12 +104,50 @@ test("a preview says the pull, and a signed-in director gets the tell link", asy | ||
| 104 | 104 | const html = await ( |
| 105 | 105 | await door(`notes--${pull}.${DOMAIN}`, `forge_grant_${record.id.slice(-8)}=${grant}`) |
| 106 | 106 | ).text(); |
| 107 | expect(html).toContain(`<p>preview of pull #${pull}</p>`); | |
| 107 | expect(html).toContain(`preview of <a href="${servers.url}/alice/notes/pulls/${pull}">pull #${pull}</a>`); | |
| 108 | 108 | expect(html).toContain(`href="${servers.url}/alice/notes/tell"`); |
| 109 | 109 | expect(html).toContain(">Tell the crew<"); |
| 110 | 110 | expect(html).not.toContain("<b>t</b>"); |
| 111 | 111 | }); |
| 112 | 112 | |
| 113 | test("a preview links the pull and shows its escaped title to a reader; a private repo shows neither", async () => { | |
| 114 | const html = await (await door(`notes--${pull}.${DOMAIN}`)).text(); | |
| 115 | expect(html).toContain( | |
| 116 | `<a href="${servers.url}/alice/notes/pulls/${pull}">pull #${pull}</a>: <b>t</b>`, | |
| 117 | ); | |
| 118 | const owner = getUserById(app.db, alice)!; | |
| 119 | const made = await newApp(app, owner, { name: "hush", description: "x", visibility: "private" }); | |
| 120 | if (!made.ok) throw new Error(String(made.error)); | |
| 121 | const hush = getAppByRepo(app.db, made.value.repo.id)!; | |
| 122 | const p = createIssue(app.db, { | |
| 123 | repoId: made.value.repo.id, | |
| 124 | authorId: alice, | |
| 125 | title: "secret plan", | |
| 126 | kind: "pull", | |
| 127 | }); | |
| 128 | if (!p.ok) throw new Error(p.error); | |
| 129 | const n = p.value.number; | |
| 130 | const target = { app: hush, env: `pull-${n}` } as AppTarget; | |
| 131 | const grant = signGrant(dataDir, target, 60_000, Date.now(), { | |
| 132 | id: alice, | |
| 133 | name: "alice", | |
| 134 | role: "admin", | |
| 135 | }); | |
| 136 | const mine = await ( | |
| 137 | await door(`hush--${n}.${DOMAIN}`, `forge_grant_${hush.id.slice(-8)}=${grant}`) | |
| 138 | ).text(); | |
| 139 | expect(mine).toContain("secret plan"); | |
| 140 | // Same facts without the reader's rights: number only. | |
| 141 | const bare = doorPage( | |
| 142 | { where: { app: "hush", env: `pull-${n}`, pull: n }, forgeUrl: servers.url }, | |
| 143 | false, | |
| 144 | ); | |
| 145 | const text = await bare.text(); | |
| 146 | expect(text).toContain(`<p>preview of pull #${n}</p>`); | |
| 147 | expect(text).not.toContain("<a href=\"" + servers.url + "/alice/hush/pulls"); | |
| 148 | expect(text).not.toContain("secret plan"); | |
| 149 | }); | |
| 150 | ||
| 113 | 151 | test("a nested host is the guest's: not found", async () => { |
| 114 | 152 | const res = await door(`other--notes.${DOMAIN}`); |
| 115 | 153 | expect(res.status).toBe(404); |
new above
offline
words.replace(n, ...) relies on doorWhere containing the literal "pull #N". It is correct today, but it is coupled to the wording in doorWhere. Building the link from where.pull directly would stop a later wording change from silently dropping the link.