Public: anyone can read this; the crew can change it.
The forge toolbar in an app's preview is pinned by tests: shown on pages, absent from data, never twice #10716
Check · Review · Land · Live done — Landed and live in production.
The toolbar is now pinned by tests. A page that already carries the toolbar tag no longer gets a second one. `tsc`, `scripts/gate.ts` and the toolbar and readme-cap tests pass locally. I did not run the full suite, and I ran no `scripts/fast.ts`; the forge's check runs the whole suite on the push.
shipped production is serving it deploy cca14ac
The toolbar is now pinned by tests. A page that already carries the toolbar tag no longer gets a second one. tsc, scripts/gate.ts and the toolbar and readme-cap tests pass locally. I did not run the full suite, and I ran no scripts/fast.ts; the forge's check runs the whole suite on the push.
The toolbar's injection already lived in src/apps/proxy.ts and src/apps/toolbar.ts, and most of what the issue asks for was already true. What I changed:
- No double injection:
withToolbarinsrc/apps/toolbar.tsnow checks whether the page already has a/__forge/toolbarscript tag. If it does, it returns the page as it is. Before, only the browser-side guard stopped a second copy. - Tests: the new
test/toolbar-injection.test.tscovers:
- Only HTML is eligible; JSON, images, CSS, JS, SVG and text pass through.
- The tag goes in once, before the last </body>, and at the end when there is no </body>.
- A page that already has the tag stays unchanged.
- Escaping of app and pull names, including a hostile title.
- The CSP behaviour: the nonce is copied onto the tag, and the tag points at the app's own host so script-src 'self' admits it.
- The pill is position: fixed in a shadow root and adds nothing to the page's layout or style sheets.
- The pull link text.
- Link back: the existing "Preview of pull #N" link to the pull's forge page and the Home link already did this, so I only added a test for the link text.
- Docs:
docs/features/forge-toolbar.mdis new and linked fromdocs/README.md.
Not done:
- The layout-shift test reads the script source for
position:fixedandattachShadow. I did not run it in a real browser. - An app whose CSP admits neither
'self'nor a nonce still gets no pill. That is the existing behaviour, and I documented it.
To try it, run bun test test/toolbar-injection.test.ts test/apps-toolbar.test.ts. On a preview, view the source of any HTML page and look for one /__forge/toolbar-…js tag.
🤖 Generated with Claude Code
Changes needed, in one round:
- The preview does not show it working.
curl -H "authorization: Bearer $FORGE_TOKEN" https://lab--10716--forge-trevato-apps.open-platform.sh/returns a 200 HTML page that ends</script></body></html>with no/__forge/toolbar-<hash>.jstag. Its CSP isscript-src 'self', which the docs say admits the tag. Find out why the tag is absent (the proxy path, or the toolbar being skipped for this response) and fix it or say precisely why. Then show the tag once in that page's source. carriesToolbardecodes the whole body to a latin1 string on every HTML response, and it matches anysrc="/__forge/toolbartext. A page that merely quotes that string, such as docs or a code sample, silently loses its pill. Search the Buffer (buffer.includes) for the full tag shape,<script ... src="/__forge/toolbar-with the hashed name, and add a test for a page that quotes the string in text and still gets one tag.- Box 1 asks that data is not injected. The test only calls
htmlAnsweron header objects. Add a test through the proxy path (theproxy.tscall site) that JSON, image, Range and HEAD responses come back byte for byte, and that an HTML response already carrying the tag comes back with exactly one. - The layout-shift test only greps the script source for
position:fixed. That is acceptable only alongside item 3, so keep it but do not count it as the layout proof.
All four points are addressed and pushed. tsc, scripts/gate.ts and the toolbar tests pass locally. I did not run the full suite; the forge's check does.
- Tag absent in the preview. The tag was not missing; the
curlrequest skipped it.
- Cause: the proxy only asks the app for an uncompressed page when the request's Accept header includes text/html. Bare curl sends /, so the app can gzip its page, and the proxy cannot rewrite a gzipped body.
- Check: I asked for the preview of #10716 with -H "accept: text/html". Its source carries exactly one tag: <script src="/__forge/toolbar-e2c4b93be4a81b0c.js" data-forge="https://forge.open-platform.sh" data-app="lab" data-repo="trevato/forge" data-env="pull-10716" data-pull="10716" …>.
- Where it shows up: browsers always send text/html, so a page opened in a browser gets the pill. Non-browser clients that send / may get a page without it.
- Not changed: I left the Accept rule alone.
- Hash: the deployed hash comes from the outer forge's proxy, not from this branch's code.
carriesToolbar. It now does a byte search withBuffer.includesfor the exact tag opening,<script src="/__forge/toolbar-<hash>.js". Nothing is decoded to a string, and a page that only quotes the path no longer matches. Bothtest/toolbar-injection.test.tsandtest/apps-toolbar.test.tshave tests for a page that quotes the string and still gets one tag.- Proxy-path test. A new test in
test/apps-toolbar.test.tsruns through the real proxy:
- JSON, a PNG, a 206 range and HEAD come back byte for byte. - A page that already has the tag comes back with exactly one.
- Layout test. I kept the source check and labelled it as a source check only. The proxy tests are the proof that data is untouched.
Preview with Accept: text/html carries exactly one toolbar tag, with the title escaped. carriesToolbar is now a Buffer byte search for the exact tag opening. The proxy-path test covers JSON, PNG, range and HEAD byte for byte, and a page that already has the tag keeps one. Docs page is added and linked from docs/README.md. Next two things: (1) the proxy skips the pill for clients that send Accept: / and get a gzipped page, so consider asking for identity encoding for any HTML-capable client; (2) the layout check is a source grep, so add a browser-level check later.
The toolbar is now pinned by tests. A page that already carries the toolbar tag no longer gets a second one.
tsc,scripts/gate.tsand the toolbar and readme-cap tests pass locally. I did not run the full suite, and I ran noscripts/fast.ts; the forge's check runs the whole suite on the push.The toolbar's injection already lived in
src/apps/proxy.tsandsrc/apps/toolbar.ts, and most of what the issue asks for was already true. What I changed:withToolbarinsrc/apps/toolbar.tsnow checks whether the page already has a/__forge/toolbarscript tag. If it does, it returns the page as it is. Before, only the browser-side guard stopped a second copy.test/toolbar-injection.test.tscovers:- Only HTML is eligible; JSON, images, CSS, JS, SVG and text pass through. - The tag goes in once, before the last
</body>, and at the end when there is no</body>. - A page that already has the tag stays unchanged. - Escaping of app and pull names, including a hostile title. - The CSP behaviour: the nonce is copied onto the tag, and the tag points at the app's own host soscript-src 'self'admits it. - The pill isposition: fixedin a shadow root and adds nothing to the page's layout or style sheets. - The pull link text.docs/features/forge-toolbar.mdis new and linked fromdocs/README.md.Not done:
position:fixedandattachShadow. I did not run it in a real browser.'self'nor a nonce still gets no pill. That is the existing behaviour, and I documented it.To try it, run
bun test test/toolbar-injection.test.ts test/apps-toolbar.test.ts. On a preview, view the source of any HTML page and look for one/__forge/toolbar-…jstag.🤖 Generated with Claude Code
Closes #10711