diff --git a/sidecar/rewrite.test.ts b/sidecar/rewrite.test.ts
index 68f7610..a70d572 100644
--- a/sidecar/rewrite.test.ts
+++ b/sidecar/rewrite.test.ts
@@ -71,6 +71,16 @@ describe('absolute URLs built from ROOT_URL', () => {
expect(rewriteHtml('', BASE, ORIGINS)).toContain(`${BASE}/explore`);
});
+ test('the JSON-ESCAPED form, which is how appUrl appears in Gitea\'s inline script', () => {
+ // `appUrl: 'https:\\/\\/gitea.officer.dev\\/'`. Missed at first, and the failure was quiet: the page
+ // rendered fine while every URL Gitea's JS built from appUrl went cross-origin to the real host and
+ // came back as its login HTML — so the heatmap reported "Unexpected token '<'" parsing JSON.
+ const real = String.raw`appUrl: 'https:\/\/gitea.officer.dev\/',`;
+ const out = rewriteHtml(real, BASE, ORIGINS);
+ expect(out).toContain(String.raw`'\/api\/gitea\/_web\/'`);
+ expect(out).not.toContain('gitea.officer.dev');
+ });
+
test('an unrelated host is left alone', () => {
const html = '';
expect(rewriteHtml(html, BASE, ORIGINS)).toBe(html);
diff --git a/sidecar/rewrite.ts b/sidecar/rewrite.ts
index 3ebb75b..9e0b0b7 100644
--- a/sidecar/rewrite.ts
+++ b/sidecar/rewrite.ts
@@ -94,11 +94,23 @@ export function rewriteAppSubUrl(html: string, base: string): string {
* configured with a public `ROOT_URL` emits THAT instead, so both are rewritten when they differ.
*/
export function rewriteAbsolute(html: string, base: string, origins: string[]): string {
+ const escapedBase = base.split('/').join('\\/');
let out = html;
for (const origin of origins.filter(Boolean)) {
- // Escaped, because an origin is user-supplied config and a stray `.` or `?` would match too much.
- const escaped = origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- out = out.replace(new RegExp(`${escaped}(?=/|"|'|\\s|$)`, 'g'), base);
+ // Regex-escaped, because an origin is user-supplied config and a stray `.` or `?` would match too much.
+ const rx = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+ // The plain form, in attributes and text.
+ out = out.replace(new RegExp(`${rx(origin)}(?=/|"|'|\\s|$)`, 'g'), base);
+
+ // And the JSON-ESCAPED form, which is how it appears inside Gitea's inline script:
+ // appUrl: 'https:\/\/gitea.example.com\/'
+ // Missed at first, and the consequence was subtle rather than visible — the page rendered, but every
+ // URL Gitea's JS built from `appUrl` pointed at the real host, cross-origin. Those requests came back
+ // as Gitea's own login HTML, so the heatmap reported "Unexpected token '<'" while parsing JSON, and
+ // the event worker failed the same way.
+ const escapedOrigin = origin.split('/').join('\\/');
+ out = out.replace(new RegExp(rx(escapedOrigin), 'g'), escapedBase);
}
return out;
}