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

Merged opened this

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

Merged

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

opened this

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: withToolbar in src/apps/toolbar.ts now checks whether the page already has a /__forge/toolbar script 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.ts covers:

- 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.md is new and linked from docs/README.md.

Not done:

  • The layout-shift test reads the script source for position:fixed and attachShadow. 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

Closes #10711

commented

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: withToolbar in src/apps/toolbar.ts now checks whether the page already has a /__forge/toolbar script 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.ts covers:

- 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.md is new and linked from docs/README.md.

Not done:

  • The layout-shift test reads the script source for position:fixed and attachShadow. 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 requested — before the latest push

Changes needed, in one round:

  1. 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>.js tag. Its CSP is script-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.
  2. carriesToolbar decodes the whole body to a latin1 string on every HTML response, and it matches any src="/__forge/toolbar text. 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.
  3. Box 1 asks that data is not injected. The test only calls htmlAnswer on header objects. Add a test through the proxy path (the proxy.ts call 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.
  4. 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.
commented

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.

  1. Tag absent in the preview. The tag was not missing; the curl request 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.

  1. carriesToolbar. It now does a byte search with Buffer.includes for 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. Both test/toolbar-injection.test.ts and test/apps-toolbar.test.ts have tests for a page that quotes the string and still gets one tag.
  2. Proxy-path test. A new test in test/apps-toolbar.test.ts runs 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.

  1. 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.
Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs