From 9ff5fa08647a62598cfe74c22e137cf76ed007f1 Mon Sep 17 00:00:00 2001 From: Travis Gilbert <1travisgilbert@gmail.com> Date: Tue, 4 Aug 2026 18:40:12 -0400 Subject: [PATCH] fix(console): rewrite OpenWork /chat HTML asset URLs Root-absolute Vite assets 404 on the console origin after the /chat proxy strips the prefix, which left a blank page after login. --- .../src/lib/chat-openwork-proxy.test.ts | 35 +++++++++++++++ apps/console/src/lib/chat-openwork-proxy.ts | 43 +++++++++++++++++++ apps/console/src/middleware.ts | 21 ++++++--- 3 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 apps/console/src/lib/chat-openwork-proxy.test.ts create mode 100644 apps/console/src/lib/chat-openwork-proxy.ts diff --git a/apps/console/src/lib/chat-openwork-proxy.test.ts b/apps/console/src/lib/chat-openwork-proxy.test.ts new file mode 100644 index 00000000..08825b58 --- /dev/null +++ b/apps/console/src/lib/chat-openwork-proxy.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { + rewriteOpenworkChatHtml, + rewriteOpenworkLocation, +} from './chat-openwork-proxy'; + +describe('rewriteOpenworkChatHtml', () => { + it('prefixes root-absolute assets and stamps the register impl', () => { + const html = ` + + + +`; + const out = rewriteOpenworkChatHtml(html); + expect(out).toContain('data-register-impl="openwork.chat"'); + expect(out).toContain('href="/chat/openwork-mark.svg"'); + expect(out).toContain('src="/chat/assets/app-abc.js"'); + expect(out).toContain('href="/chat/assets/index-abc.css"'); + expect(out).not.toContain('src="/assets/'); + }); + + it('does not double-prefix paths already under /chat', () => { + const html = ``; + expect(rewriteOpenworkChatHtml(html)).toContain('src="/chat/assets/x.js"'); + expect(rewriteOpenworkChatHtml(html)).not.toContain('src="/chat/chat/'); + }); +}); + +describe('rewriteOpenworkLocation', () => { + it('prefixes absolute redirects', () => { + expect(rewriteOpenworkLocation('/')).toBe('/chat/'); + expect(rewriteOpenworkLocation('/settings')).toBe('/chat/settings'); + expect(rewriteOpenworkLocation('/chat/x')).toBe('/chat/x'); + }); +}); diff --git a/apps/console/src/lib/chat-openwork-proxy.ts b/apps/console/src/lib/chat-openwork-proxy.ts new file mode 100644 index 00000000..a94c01b9 --- /dev/null +++ b/apps/console/src/lib/chat-openwork-proxy.ts @@ -0,0 +1,43 @@ +/** Rewrite OpenWork HTML so root-absolute assets load under the /chat proxy prefix. + * + * The workspace Vite app emits `/assets/...` and `/favicon-...` URLs. Middleware + * only proxies `/chat/*`, so those root paths 404 on the console origin and the + * page paints blank. Prefix them with `/chat` (already forwarded upstream). + */ +export function rewriteOpenworkChatHtml(html: string): string { + let out = html.includes('data-register-impl=') + ? html + : html.replace( + /]*)>/i, + '', + ); + + // Attribute URLs: src="/assets/x", href="/favicon.png", etc. + out = out.replace( + /\b(href|src|poster)=(["'])\/(?!\/|chat\/)/g, + '$1=$2/chat/', + ); + + // Inline modulepreload / import maps occasionally use content URLs. + out = out.replace( + /\b(url)\((["']?)\/(?!\/|chat\/)/g, + '$1($2/chat/', + ); + + return out; +} + +export function rewriteOpenworkLocation(location: string | null): string | null { + if (!location) return location; + if (location.startsWith('/chat/') || location === '/chat') return location; + if (location.startsWith('/')) return `/chat${location === '/' ? '/' : location}`; + try { + const url = new URL(location); + if (!url.pathname.startsWith('/chat')) { + url.pathname = `/chat${url.pathname === '/' ? '/' : url.pathname}`; + } + return url.toString(); + } catch { + return location; + } +} diff --git a/apps/console/src/middleware.ts b/apps/console/src/middleware.ts index 26dc1ee5..d42a4be8 100644 --- a/apps/console/src/middleware.ts +++ b/apps/console/src/middleware.ts @@ -1,10 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; +import { + rewriteOpenworkChatHtml, + rewriteOpenworkLocation, +} from '@/lib/chat-openwork-proxy'; // SOURCING: none. SPEC-COMMONPLACE-PRODUCTION-CUTOVER-1.0 GL6 / OW4. // When CONSOLE_WORKSPACE_URL is set, /chat is reverse-proxied to the workspace // chat door with the /chat prefix stripped. Cookie stays on the console origin. // NextResponse.rewrite cannot target an arbitrary external origin here, so this -// is an explicit fetch proxy. +// is an explicit fetch proxy. HTML root-absolute asset URLs are rewritten under +// /chat so Vite bundles do not 404 on the console origin (blank OpenWork page). const WORKSPACE = process.env.CONSOLE_WORKSPACE_URL?.replace(/\/$/, '') ?? ''; @@ -60,14 +65,16 @@ export async function middleware(request: NextRequest) { const responseHeaders = new Headers(upstream.headers); responseHeaders.set('x-register-impl', 'openwork.chat'); + const location = upstream.headers.get('location'); + if (location) { + responseHeaders.set('location', rewriteOpenworkLocation(location) ?? location); + } + if (upstreamContentType.includes('text/html')) { const html = await upstream.text(); - const stamped = html.includes('data-register-impl=') - ? html - : html.replace( - /]*)>/i, - '', - ); + const stamped = rewriteOpenworkChatHtml(html); + // Content-Length from upstream is stale after rewrite. + responseHeaders.delete('content-length'); return new NextResponse(stamped, { status: upstream.status, headers: responseHeaders,