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 });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
| Option | Type | Default | Description |
|---|---|---|---|
root | string | — | The one directory the server will ever touch. Resolved to a real path once, at startup. |
base | string | "/" | Path prefix the routes hang off. |
readonly | boolean | false | Refuses every mutating route with EPERM. Turned on for you automatically if root sits on a read-only mount. |
exclude | string[] | [".git"] | Glob patterns never listed, read, written or watched. A directory matched by a pattern takes its whole subtree with it. |
maxFileSize | number | 4194304 (4 MiB) | Bytes allowed in either direction. A file over this is listed but not read or written. |
hidden | boolean | true | Include 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. excludeis enforced everywhere. A matched file or directory is never listed, read, written or watched — it behaves as if it doesn't exist.maxFileSizecaps both directions. A write over the limit is rejected before the body is read; a read over the limit answersETOOBIG.readonlyrefuses every mutating route.write,mkdir,moveanddeleteall answerEPERMimmediately, 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.
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.