
# Server

`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](/extensions/remote)
to back a workbench with real files.

## The smallest example

```ts
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.

::tabs
::tab{label="Fetch handler"}

```ts
import { CodeletServer } from "codelet/server";

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

Bun.serve({ fetch: server.fetch });
```

::
::tab{label="Nitro"}

```ts [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

| 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`:

```ts
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](/extensions/remote) 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.

```ts [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{to="/extensions/remote"}
