chore: version packages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-07-29 17:50:41 +02:00
parent a406aef2fa
commit af9db455ff
14 changed files with 285 additions and 48 deletions
+37 -1
View File
@@ -159,12 +159,48 @@ function hasValidWebviewCapability(req: FastifyRequest): boolean {
// capability already implies an authenticated `POST /api/webviews/:id/open`, but
// the exemption should stay no wider than the problem it solves.
if (req.method !== 'GET' && req.method !== 'HEAD') return false;
if (url.startsWith('/api/') || url.startsWith('/ws/') || url.startsWith('/q/')) return false;
if (url.startsWith('/ws/') || url.startsWith('/q/')) return false;
// Anything that resolves to a REAL Codeman route is refused, which is the fence
// that keeps this from being an auth bypass. `/api/` used to be refused by prefix
// instead, but dashboards legitimately serve assets from their own `/api/...`
// namespace (`<img src="/api/hero?slug=x">`), and those requests were the one
// class the 404 relay could never rescue. See matchesRegisteredRoute.
if (matchesRegisteredRoute(req, url)) return false;
const fromReferer = capabilityFromReferer(typeof req.headers.referer === 'string' ? req.headers.referer : undefined);
return !!fromReferer && webviewCapabilities.resolve(fromReferer) !== undefined;
}
/**
* Whether `url` resolves to a route Codeman actually registered.
*
* `hasRoute()` is the wrong tool: it matches the registered PATTERN literally, so
* `/api/sessions/abc` reports false against a registered `/api/sessions/:id` and
* would hand out an exemption on a live API route. `findRoute()` performs the real
* radix-tree lookup and fills in `params`, which is what this needs.
*
* The one complication is `@fastify/static`, mounted at `/`, which registers a
* root-level catch-all that matches EVERY path. A match on that means "no real
* route, this is heading for the 404 handler", and it is distinguishable because a
* root catch-all is the only route whose `*` param comes back equal to the entire
* request path. `test/webview-auth-exemption.test.ts` pins both halves of that.
*
* Fails CLOSED: anything unexpected counts as a real route, which merely denies the
* exemption and restores the previous behavior.
*/
function matchesRegisteredRoute(req: FastifyRequest, url: string): boolean {
try {
const found = req.server.findRoute({ method: req.method as 'GET' | 'HEAD', url });
if (!found) return false;
const params = found.params ?? {};
const keys = Object.keys(params);
const isRootCatchAll = keys.length === 1 && keys[0] === '*' && `/${params['*']}` === url;
return !isRootCatchAll;
} catch {
return true;
}
}
/**
* Register HTTP Basic Auth middleware with session cookies and rate limiting.
* Only active when CODEMAN_PASSWORD is set.
+26 -1
View File
@@ -3546,7 +3546,12 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
padding: 4px;
z-index: 1000;
min-width: 190px;
max-width: 300px;
/* Every long row inside (recent-session paths, saved URL names) ellipsizes, so
the cap is a deliberate choice rather than a fit-the-content result: the menu
overlays the terminal and does not need to reach across it. 250 rather than a
rounder 240 because it is the width at which the common `~/<dir>/<repo>` +
timestamp recent-session row still fits whole, which is what that list is for. */
max-width: 250px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
}
.run-mode-menu.active {
@@ -3611,8 +3616,19 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
max-height: 200px;
overflow-y: auto;
}
/* `overflow-y: auto` computes overflow-x to auto as well, which makes this a scroll
container and lets a row size itself to its own content. A long path therefore
scrolled sideways instead of ellipsizing, and before the menu was narrowed it
simply pinned the menu at its max-width. */
.run-mode-history .run-mode-option {
max-width: 100%;
}
.run-mode-option .hist-dir {
flex: 1;
/* A flex item's default min-width is auto, so without this the path refuses to
shrink below its own text and pushes the date out of the menu instead of
ellipsizing. Only visible once the menu is narrow enough to force the choice. */
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -12575,7 +12591,16 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
}
/* The label needs its own element: `text-overflow` on the button does nothing,
because the button is a flex container and a bare text node in one becomes an
anonymous flex item that ellipsis cannot reach. Without this a long dashboard
name widens the whole menu instead of truncating. */
.run-mode-web-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.run-mode-row-btn {
flex: 0 0 auto;
+1 -1
View File
@@ -312,7 +312,7 @@ Object.assign(CodemanApp.prototype, {
const icon = w.icon ? escapeHtml(w.icon) : '<span class="run-mode-dot web"></span>';
return `<div class="run-mode-row run-mode-row--web">
<button class="run-mode-option run-mode-option--web" onclick="app.openWebviewFromMenu(${jsonId})" title="${escapeHtml(w.url)}">
<span class="run-mode-menu-icon">${icon}</span>${name}
<span class="run-mode-menu-icon">${icon}</span><span class="run-mode-web-name">${name}</span>
</button>
<button class="run-mode-row-btn run-mode-webview-edit" onclick="event.stopPropagation(); app.showWebviewModal(${jsonId})"
title="Edit URL" aria-label="Edit ${name}">&#x2699;</button>
+12 -5
View File
@@ -855,14 +855,21 @@ export class WebServer extends EventEmitter {
// the envelope hook into a contradictory HTTP 404 {success:true,...}.
this.app.setNotFoundHandler(async (req, reply) => {
const notFound = `Route ${req.method}:${req.url} not found`;
// A web-tab dashboard asking for a root-absolute asset (`fetch('/api/data')`,
// `import('/chunk.js')`, `url(/img.png)` inside a stylesheet) lands here,
// because `<base href>` cannot rewrite a URL built at runtime. Its Referer says
// which dashboard to relay to. Deliberately placed on the 404 path so every
// real Codeman route still wins.
//
// Tried BEFORE the API-shaped 404, because a dashboard's own assets commonly
// live under its `/api/...` namespace and were the one class this could never
// rescue. Reaching this handler at all already proves no Codeman route matched,
// and the relay declines unless the Referer carries a live capability, so
// genuinely unknown `/api` paths still get the envelope below.
if (await tryWebviewRefererFallback(req, reply)) return reply;
if (req.url.startsWith('/api')) {
return reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, notFound));
}
// A web-tab dashboard asking for a root-absolute asset (`fetch('/api/data')`,
// `import('/chunk.js')`) lands here, because `<base href>` cannot rewrite a URL
// built at runtime. Its Referer says which dashboard to relay to. Deliberately
// placed on the 404 path so every real Codeman route still wins.
if (await tryWebviewRefererFallback(req, reply)) return reply;
return reply.code(404).send({ message: notFound, error: 'Not Found', statusCode: 404 });
});
+27 -7
View File
@@ -366,11 +366,11 @@ export function buildDownstreamResponseHeaders(
* root, where it 404s. That is not a rare shape: it is how most dashboards talk to
* their own backend, and it presents as the dashboard's own "Failed to fetch".
*
* The `Referer`-keyed 404 fallback catches some of these, but deliberately NOT
* paths under `/api`, `/ws` or `/q` (widening it there would let a request-supplied
* header skip auth on Codeman's own API). Rewriting inside the iframe removes the
* whole class instead of trading security for it: the page never emits a
* root-absolute request in the first place.
* The `Referer`-keyed 404 fallback catches some of these, but it is a rescue rather
* than a fix (it only fires for a request that already missed every Codeman route,
* and only when the browser sends a usable `Referer`). Rewriting inside the iframe
* removes the whole class instead: the page never emits a root-absolute request in
* the first place.
*
* ## Why the DOM sinks are patched too, not just fetch/XHR
*
@@ -455,6 +455,14 @@ function rwAttr(n,v){
return A.indexOf(k)===-1?v:rw(v);
}catch(e){return v;}
}
// CSS built at runtime is the one sink NO relay can rescue: a <style> element has
// no URL of its own, so an opaque-origin document sends an EMPTY Referer with the
// resulting image request, and the 404 fallback has nothing to key on.
function rwCss(s){
try{
return String(s).replace(/url\\(\\s*(['"]?)(\\/(?!\\/)[^'")]*)\\1\\s*\\)/gi,function(m,q,u){return 'url('+q+rw(u)+q+')';});
}catch(e){return s;}
}
// Each value goes through rw() rather than a blind prefix concat, because unlike
// the server-side rewriteHtml() this runs on markup that may ALREADY be proxied
// (a page re-injecting its own outerHTML), and rw() is the idempotent one.
@@ -465,7 +473,8 @@ function rwHtml(s){
.replace(/(\\s(?:src|href|action|poster|formaction|data)\\s*=\\s*")([^"]*)(")/gi,function(m,a,v,q){return a+rw(v)+q;})
.replace(/(\\s(?:src|href|action|poster|formaction|data)\\s*=\\s*')([^']*)(')/gi,function(m,a,v,q){return a+rw(v)+q;})
.replace(/(\\ssrcset\\s*=\\s*")([^"]*)(")/gi,function(m,a,v,q){return a+rwSet(v)+q;})
.replace(/(\\ssrcset\\s*=\\s*')([^']*)(')/gi,function(m,a,v,q){return a+rwSet(v)+q;});
.replace(/(\\ssrcset\\s*=\\s*')([^']*)(')/gi,function(m,a,v,q){return a+rwSet(v)+q;})
.replace(/(<style\\b[^>]*>)([^]*?)(<\\/style>)/gi,function(m,a,b,c){return a+rwCss(b)+c;});
}catch(e){return s;}
}
// Marked with __cmrw so a double injection (a page that re-runs the shim) cannot
@@ -530,12 +539,23 @@ try{
}
}catch(e){}
};
var fixStyle=function(el){
try{
if(!el||el.tagName!=='STYLE')return;
var t=el.textContent;
if(!t||t.indexOf('url(')===-1)return;
var n=rwCss(t);
if(n!==t)el.textContent=n;
}catch(e){}
};
var scan=function(node){
try{
fix(node);
fix(node);fixStyle(node);
if(node&&node.querySelectorAll){
var l=node.querySelectorAll('[src],[href],[action],[poster],[data],[srcset],[formaction]');
for(var i=0;i<l.length;i++)fix(l[i]);
var st=node.querySelectorAll('style');
for(var j=0;j<st.length;j++)fixStyle(st[j]);
}
}catch(e){}
};