remove projects and apps end to end

deletes the last of the projects/apps cluster: the published-app store
(/api/apps + /api/app-serve), the project dev-server and its websocket
proxy (/api/dev-server + /api/dev-server-proxy), the shared html-rewrite
they were the only consumers of, and their frontend — the Preview panel,
the UserApp panel/header, useUserApps and the /settings/apps screen.

also drops getUserProjectsDir and getUserAppsDir, the ProjectType and
ProjectDefinition types, and the 'dev-server' websocket provider from
server.tsx. nothing on disk is touched.

1440 deletions, 31 insertions. tsgo clean.
This commit is contained in:
2026-07-31 07:39:29 +00:00
parent 13b7f56b0f
commit 188b45c113
28 changed files with 31 additions and 1440 deletions
-116
View File
@@ -11,7 +11,6 @@ import { pipelineWebsocket } from './servers/api/tasks/pipeline-executor';
import { cliampWebsocket, cliampAudioWebsocket } from './servers/api/cliamp/relay';
import { desktopWebsocket } from './servers/api/desktop/websocket';
import { vaultWebsocket, upgradeVaultWs } from './servers/api/vault/websocket';
import { findEntryByProxyId, touchEntry } from './servers/api/dev-server/router';
import officerWeb from './apps/officer-web/index.gen.html';
import { startBrowserRelay } from './servers/api/browser/relay';
import { registerSidecar, unregisterSidecar, handleSidecarMessage } from './servers/sidecar-registry';
@@ -38,7 +37,6 @@ type WSData = {
| 'chat'
| 'task-runner'
| 'pipeline'
| 'dev-server'
| 'cliamp'
| 'cliamp-audio'
| 'desktop'
@@ -50,10 +48,6 @@ type WSData = {
cols?: number;
rows?: number;
search?: string; // raw query string, for providers that relay it to a sidecar
devServerPort?: number;
devServerSlug?: string;
wsProxyPath?: string;
wsToken?: string;
};
// Sidecar registration WebSocket handler
@@ -150,83 +144,6 @@ const handlers: Record<string, any> = {
sidecar: sidecarWebsocket,
};
// Dev-server WebSocket proxy: bridges client WS ↔ upstream dev server WS (for HMR etc.)
type UpstreamState = { ws: WebSocket; queue: (string | Buffer)[]; ready: boolean };
// Bun hands WS frames over as `string | Buffer`, but the DOM WebSocket.send signature won't accept a
// Buffer<ArrayBufferLike> (it can't rule out a SharedArrayBuffer backing). A Buffer is a Uint8Array
// at runtime, so this forwards as-is rather than paying for a copy on every proxied frame.
const asWsPayload = (raw: string | Buffer): string | Uint8Array<ArrayBuffer> =>
typeof raw === 'string' ? raw : (raw as Uint8Array<ArrayBuffer>);
const devServerUpstreams = new Map<ServerWebSocket<WSData>, UpstreamState>();
const devServerWebsocket = {
async open(ws: ServerWebSocket<WSData>) {
const { devServerPort, wsProxyPath, wsToken } = ws.data;
// Validate JWT (deferred from upgrade which must be synchronous in Bun)
if (!wsToken) {
ws.close(4001, 'Unauthorized');
return;
}
try {
const payload = await verify(wsToken);
if (!payload) {
ws.close(4001, 'Unauthorized');
return;
}
if (payload.jti && (await isTokenBlacklisted(payload.jti))) {
ws.close(4001, 'Unauthorized');
return;
}
} catch {
ws.close(4001, 'Unauthorized');
return;
}
const upstream = new WebSocket(`ws://localhost:${devServerPort}${wsProxyPath}`);
const state: UpstreamState = { ws: upstream, queue: [], ready: false };
devServerUpstreams.set(ws, state);
upstream.addEventListener('open', () => {
state.ready = true;
for (const msg of state.queue) upstream.send(asWsPayload(msg));
state.queue.length = 0;
});
upstream.addEventListener('message', (event) => {
ws.send(event.data as string | ArrayBuffer);
});
upstream.addEventListener('close', () => {
devServerUpstreams.delete(ws);
ws.close();
});
upstream.addEventListener('error', () => {
devServerUpstreams.delete(ws);
ws.close();
});
},
message(ws: ServerWebSocket<WSData>, raw: string | Buffer) {
const state = devServerUpstreams.get(ws);
if (!state) return;
if (state.ready) {
state.ws.send(asWsPayload(raw));
} else {
state.queue.push(raw);
}
},
close(ws: ServerWebSocket<WSData>) {
const state = devServerUpstreams.get(ws);
if (state) {
state.ws.close();
devServerUpstreams.delete(ws);
}
},
};
handlers['dev-server'] = devServerWebsocket;
async function upgradeWs(
req: Request,
server: any,
@@ -269,35 +186,6 @@ async function upgradeWs(
}
}
function upgradeDevServerWs(req: Request, server: any) {
const url = new URL(req.url);
const match = url.pathname.match(/^\/api\/dev-server-proxy\/([^/]+)(\/.*)?$/);
if (!match) return new Response('Not found', { status: 404 });
const proxyId = match[1]!;
const entry = findEntryByProxyId(proxyId);
if (!entry) return new Response('No dev server running', { status: 404 });
const wsToken = url.searchParams.get('token');
if (!wsToken) return new Response('Unauthorized', { status: 401 });
touchEntry(entry);
const wsProxyPath = match[2] || '/';
const ok = server.upgrade(req, {
data: {
userId: 0,
email: '',
provider: 'dev-server' as const,
devServerPort: entry.port,
devServerSlug: proxyId,
wsProxyPath,
wsToken,
},
});
if (!ok) return new Response('Upgrade failed', { status: 500 });
}
const server = serve({
port: Number(PORT),
idleTimeout: 60,
@@ -312,10 +200,6 @@ const server = serve({
const file = Bun.file(`public${new URL(req.url).pathname}`);
return new Response(file);
},
'/api/dev-server-proxy/*': (req, server) => {
if (req.headers.get('upgrade') === 'websocket') return upgradeDevServerWs(req, server);
return honoServer.fetch(req, server);
},
// Vaultwarden notifications hub: upgrade the WebSocket here (proxied to upstream by vaultWebsocket);
// everything else on this path (SignalR long-poll negotiate/poll) falls through to the HTTP proxy.
'/api/vault/notifications/*': (req, server) => {