diff --git a/doc/cloud-ui-snippet.md b/doc/cloud-ui-snippet.md index 28deab517c..e7f0db9ae1 100644 --- a/doc/cloud-ui-snippet.md +++ b/doc/cloud-ui-snippet.md @@ -10,6 +10,25 @@ application origin and is visible to every browser that receives the UI shell. Do not include secrets or customer data. Restart the app after changing it. Operators must review scripts and any required CSP changes before deployment. +## Base64 variant + +Delivery pipelines that write env vars through provider APIs can sit behind +web application firewalls that reject values containing raw script markup. +`PAPERCLIP_CLOUD_UI_SNIPPET_B64` carries the same snippet through them as +standard base64 of the UTF-8 HTML: + +```sh +PAPERCLIP_CLOUD_UI_SNIPPET_B64="$(base64 < snippet.html)" +``` + +Whitespace and line wrapping in the value are tolerated. A value that is not +canonical padded base64 of UTF-8 text, or that decodes to blank, is ignored — +if the widget does not appear, check that the value round-trips through +`base64 -d`. A present `PAPERCLIP_CLOUD_UI_SNIPPET` always wins, blank +included: clearing the plain variable to blank disables injection even while +a base64 value is still deployed. Everything else about the snippet is +unchanged. + ## Plain closed beta Set the value to this standard embed, replacing `YOUR_CHAT_APP_ID` with the diff --git a/server/src/__tests__/cloud-ui-snippet.test.ts b/server/src/__tests__/cloud-ui-snippet.test.ts index 8d64f7182b..12c2e0b66f 100644 --- a/server/src/__tests__/cloud-ui-snippet.test.ts +++ b/server/src/__tests__/cloud-ui-snippet.test.ts @@ -3,10 +3,12 @@ import { injectCloudUiSnippet } from "../cloud-ui-snippet.js"; const html = '
'; const snippet = ''; +const encoded = Buffer.from(snippet, "utf-8").toString("base64"); describe("Cloud UI snippet", () => { it("leaves self-hosted HTML unchanged even when a snippet is configured", () => { expect(injectCloudUiSnippet(html, { PAPERCLIP_CLOUD_UI_SNIPPET: snippet })).toBe(html); + expect(injectCloudUiSnippet(html, { PAPERCLIP_CLOUD_UI_SNIPPET_B64: encoded })).toBe(html); }); it.each([ @@ -27,4 +29,55 @@ describe("Cloud UI snippet", () => { expect(result).toContain(script); expect(result).not.toContain("test-token"); }); + + it("decodes a base64 snippet on a Cloud instance", () => { + expect(injectCloudUiSnippet(html, { + PAPERCLIP_MANAGED_CONFIG: "{}", PAPERCLIP_CLOUD_UI_SNIPPET_B64: encoded, + })).toBe(html.replace("