mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
feat(web): support a reverse-proxy base URL (--base-url / CODEMAN_BASE_URL)
Codeman can now be mounted under a sub-path behind a reverse proxy that forwards the prefix unchanged (e.g. https://host/codeman/). Default is `/` (root), which is byte-identical to the historical behavior. Design — few choke points, mirrored ingress/egress: - src/config/base-path.ts: pure single-source normalize/validate/join/strip. - Server ingress: stripBasePath() inside Fastify rewriteUrl, so routes stay declared prefix-agnostic; un-prefixed requests (hooks, health, docker bridge hitting the raw port) pass through unchanged. - Server egress: one onSend hook prepends the base to root-absolute Location headers (covers all redirects). - HTML: renderIndexHtml points <base href> at the mount and injects window.__CODEMAN_BASE__ — ONLY when a base is set (inert at root). - Frontend runtime URLs: CodemanBase.url() route builder in constants.js, applied transparently by a fetch wrapper and explicitly at the EventSource/WebSocket/window.open/<img|iframe|a>-src sites. - sw.js derives its base from self.location; manifest uses relative start_url/scope. - Web-tab proxy: proxyPrefixFor(cap, basePath) is the single base-aware root that cascades to the injected <base>, HTML/attr rewrites, runtimeUrlShim, Set-Cookie Path and Location; capabilityFromReferer strips the base off the browser Referer, while the ingress parsers stay base-agnostic (rewriteUrl already stripped it). --base-url rides the daemon relaunch (buildWebArgs) and the service unit (resolveServicePlan). constants.js is guarded against a missing `window` for isolated unit-test contexts. Tests: test/base-path.test.ts (pure helpers), base-path coverage in webview-proxy/render-index-html/daemon-control; CodemanBase stubbed in the vm-isolated panels-ui test contexts. Docs: Remote-Access.md (sub-path section + nginx example), security-architecture.md env table, CLAUDE.md pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XUkPBxbumnct6qSrx4JDju
This commit is contained in:
committed by
Michael M. Tiller
co-authored by
Claude Opus 4.8
parent
6f7add7ce4
commit
7e4914d991
@@ -22,6 +22,63 @@
|
||||
|
||||
// Codeman — Shared constants and utility functions for frontend modules
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Reverse-proxy base path
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// When Codeman is served behind a reverse proxy under a sub-path (e.g. /codeman/),
|
||||
// the server injects `window.__CODEMAN_BASE__` (normalized: '' for root, or '/foo').
|
||||
// The `<base href>` tag in index.html already rewrites the RELATIVE asset refs, but
|
||||
// every URL the frontend builds at RUNTIME is root-absolute (`/api/...`, `/ws/...`)
|
||||
// and root-absolute URLs ignore `<base>` — so those must be prefixed here instead.
|
||||
// Rather than touch ~190 call sites, all runtime URL construction routes through this
|
||||
// ONE choke point: `CodemanBase.url()` is the route builder, and a thin wrapper over
|
||||
// `fetch` applies it transparently. The handful of EventSource/WebSocket sites call
|
||||
// `CodemanBase.url()` / `CodemanBase.base` explicitly. No-op when mounted at root.
|
||||
const CodemanBase = (function () {
|
||||
// `window` is absent in some unit-test vm contexts that load this module in
|
||||
// isolation; guard so the module still evaluates (base degrades to root).
|
||||
const _win = typeof window !== 'undefined' ? window : undefined;
|
||||
const base = String((_win && _win.__CODEMAN_BASE__) || '').replace(/\/+$/, '');
|
||||
/**
|
||||
* Prefix a root-absolute application path with the mount base. Leaves untouched:
|
||||
* relative paths and fragments/queries (resolved against `<base>`), protocol-relative
|
||||
* (`//host`) and absolute URLs, and paths already carrying the prefix.
|
||||
*/
|
||||
function url(path) {
|
||||
if (!base) return path;
|
||||
if (typeof path !== 'string' || path.length === 0) return path;
|
||||
if (path[0] !== '/') return path; // relative / fragment / query
|
||||
if (path[1] === '/') return path; // protocol-relative
|
||||
if (path === base || path.startsWith(base + '/') || path.startsWith(base + '?')) return path;
|
||||
return base + path;
|
||||
}
|
||||
return { base, url };
|
||||
})();
|
||||
if (typeof window !== 'undefined') window.CodemanBase = CodemanBase;
|
||||
|
||||
// Transparently prefix root-absolute app paths on every fetch, so the many
|
||||
// `/api/...` string literals across the frontend need no per-call edit.
|
||||
if (typeof window !== 'undefined' && CodemanBase.base && typeof window.fetch === 'function') {
|
||||
const _origFetch = window.fetch.bind(window);
|
||||
window.fetch = function (input, init) {
|
||||
if (typeof input === 'string') return _origFetch(CodemanBase.url(input), init);
|
||||
if (typeof Request !== 'undefined' && input instanceof Request) {
|
||||
try {
|
||||
const u = new URL(input.url);
|
||||
if (u.origin === location.origin) {
|
||||
const prefixed = CodemanBase.url(u.pathname);
|
||||
if (prefixed !== u.pathname) {
|
||||
return _origFetch(new Request(u.origin + prefixed + u.search + u.hash, input), init);
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
/* not a parseable URL — fall through */
|
||||
}
|
||||
}
|
||||
return _origFetch(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Web Push Utilities
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user