Codelet logoCodelet

Server

Serve a real filesystem to the browser over fetch, so a workbench can read and write files on a machine

codelet/server turns a directory on disk into a filesystem the browser can talk to. It takes a Request and returns a Response, built on node:fs/promises, so it runs on Node, Bun and Deno without pulling in any of them by name. Pair it with the Remote extension to back a workbench with real files.

#The smallest example

import { CodeletServer } from "codelet/server";

const server = new CodeletServer({ root: "./workspace" });

const response = await server.fetch(request);

server.fetch is the whole surface. Mount it wherever your runtime expects a fetch handler.

import { CodeletServer } from "codelet/server";

const server = new CodeletServer({ root: "./workspace" });

Bun.serve({ fetch: server.fetch });
api/codelet.ts
import { CodeletServer } from "codelet/server";
import { defineHandler } from "nitro";

export const server = new CodeletServer({
  root: ".",
  base: "/api/codelet/",
  exclude: ["node_modules", ".output"],
});

export default defineHandler((event) => server.fetch(event.req));

Nitro is the path this library is tested against. Route the whole base prefix (for example /api/codelet/**) to the handler — the routes underneath it are info, tree, read, write, mkdir, move, delete and watch.

#Options

OptionTypeDefaultDescription
rootstringThe one directory the server will ever touch. Resolved to a real path once, at startup.
basestring"/"Path prefix the routes hang off.
readonlybooleanfalseRefuses every mutating route with EPERM. Turned on for you automatically if root sits on a read-only mount.
excludestring[][".git"]Glob patterns never listed, read, written or watched. A directory matched by a pattern takes its whole subtree with it.
maxFileSizenumber4194304 (4 MiB)Bytes allowed in either direction. A file over this is listed but not read or written.
hiddenbooleantrueInclude dotfiles in directory listings. A dotfile asked for by name is still served even when this is false.

#Security

Warning

codelet/server has no authentication built in. Anyone who can reach these routes can read and write every file under root that exclude and readonly don't rule out. Add authentication before exposing this on a network you don't trust.

There's no CORS support either. Both are left out on purpose: a library that guessed at either would be wrong for most hosts, in a way nobody could turn off. Add them by wrapping fetch:

import { CodeletServer } from "codelet/server";

const server = new CodeletServer({ root: "./workspace" });

async function fetch(request: Request): Promise<Response> {
  const session = await getSession(request);
  if (!session) return new Response("Unauthorized", { status: 401 });
  const response = await server.fetch(request);
  response.headers.set("Access-Control-Allow-Origin", "https://your-app.example");
  return response;
}

What the server does guarantee, without any setup:

  • One directory. Every path is checked and resolved inside root; a symlink pointing outside it is refused rather than followed.
  • exclude is enforced everywhere. A matched file or directory is never listed, read, written or watched — it behaves as if it doesn't exist.
  • maxFileSize caps both directions. A write over the limit is rejected before the body is read; a read over the limit answers ETOOBIG.
  • readonly refuses every mutating route. write, mkdir, move and delete all answer EPERM immediately, without touching the filesystem.

#Watching for changes

A GET to the server's watch route opens a server-sent events stream. Each message carries a batch of changes (which paths were created, changed or deleted), coalesced so a large checkout doesn't flood the connection.

You don't call this route directly. The Remote extension subscribes to it for you whenever watch is enabled (the default) and applies each change into the workbench's tree, so an edit made outside the browser shows up without a reload.

#WebSockets

WebSocketTransport answers the same routes over a single socket instead of one request per call, for hosts that would rather hold one connection open than juggle many. It's an adapter, not a second server: a frame is turned back into the Request it represents, handed to server.fetch, and the Response goes back out over the socket — so there's one router and one set of error codes either way.

api/codelet-ws.ts
import { WebSocketTransport } from "codelet/server";
import { defineWebSocketHandler } from "nitro";
import { server } from "./codelet.ts";

export default defineWebSocketHandler(new WebSocketTransport(server));

Note

With Nitro, turn on features.websocket in nitro.config.ts and route the socket's own path ahead of the base prefix, since both would otherwise match the same URL.

On the browser side, pass websocketTransport to the Remote extension to use the same socket instead of one request per call.

Read more in Extensions > Remote.