# Migration Guide

This guide documents breaking changes and migration steps for the `@visulima/storage` package.

## Upgrading past 2.0.23: file listing is opt-in (security fix)

### Summary

- **`GET` no longer lists files by default.** Pass `allowList: true` to a handler to get the old behaviour back.
- **`PUT` only creates files under ids made of letters, digits, `_` and `-`.** Updating an existing file is unchanged.
- **Malformed `Content-Length`, `X-Total-Size` and `X-Chunk-Offset` headers are rejected** instead of being half-parsed.
- **New:** the fetch handlers (Hono, Next.js, SolidStart, Bun, Deno, Cloudflare) now serve `GET` for files. They answered `500` before.

If you never listed files over HTTP and never created files with `PUT` under ids containing dots (apart from the file extension), there is nothing to change.

### Why this is a security fix

`GET` on the collection path (for example `GET /files`) returned every stored file's id, original name and metadata, to every caller and across all users. On these handlers the file id is effectively the access key: anyone who has it can download, overwrite (`PUT`) or delete the file. The listing therefore handed out access to every stored file. Putting authentication middleware in front did not help either: the list is not limited to the caller's own files, so any signed-in user could see and reach everyone's uploads.

### File listing is opt-in

When listing is disabled (the new default), `GET` on a path that does not address a stored file answers `404`. That covers the collection path and unknown ids. A missing nanoid-style id now gives `404` too, where it used to fall back to the file list.

#### Before

```ts
import { Rest } from "@visulima/storage/handler/http/node";

const rest = new Rest({ storage });

app.use("/files", rest.handle); // GET /files → every stored file
```

#### After

Only if every caller may see every file, and only behind authentication:

```ts
import { Rest } from "@visulima/storage/handler/http/node";

const rest = new Rest({ allowList: true, storage });

app.use("/files", requireAuth, rest.handle);
```

The option works the same on every handler (`Rest`, `Multipart`, `Tus`) and runtime, including the framework helpers:

```ts
// Next.js
const handler = createNextjsHandler({ allowList: true, storage, type: "rest" });

// SolidStart
const handler = createSolidStartHandler({ allowList: true, storage, type: "rest" });

// Hono
createStorageHandler(app, { allowList: true, path: "/files", storage, type: "rest" });

// Fetch runtimes (Bun, Deno, Cloudflare)
const rest = new Rest({ allowList: true, storage }); // from "@visulima/storage/handler/http/fetch"
```

If you need a per-user listing, keep `allowList` off and expose your own endpoint that filters `storage.list()` by the authenticated user (see [Authenticated uploads](https://visulima.com/docs/packages/storage/authenticated-file-uploads)).

#### `@visulima/storage-client` list hooks

`useGetFileList` (React, Vue) and `createGetFileList` (Solid, Svelte) call this endpoint, so they get `404` unless the server enables `allowList`.

### `PUT` ids

When `PUT` creates a file that does not exist yet, the id from the URL (after its trailing extension is stripped) must match `^[\w-]{1,255}$`. Other ids are rejected with `400`. A dotted id such as `<victimId>.META` could collide with the metadata sidecar `DiskStorage` keeps next to each file and overwrite another file's metadata. Updating a file that already exists is unchanged. Ids generated by `POST` are never affected.

```diff
- PUT /files/report.v2.pdf  → 201 (id "report.v2")
+ PUT /files/report.v2.pdf  → 400
+ PUT /files/report-v2.pdf  → 201 (id "report-v2")
```

### Stricter header parsing

`Content-Length`, `X-Total-Size` and `X-Chunk-Offset` must now be plain non-negative integers. A value like `12garbage` used to be read as `12`. Now:

- a malformed `Content-Length` answers `400`
- a malformed `X-Total-Size` no longer starts chunk tracking
- a malformed `X-Chunk-Offset` answers `400`

Well-behaved clients, including `@visulima/storage-client`, are not affected.

### Other changes

- **Encoded traversal ids:** ids like `..%2F..%2Fetc` are rejected with `400` on every method. `GET` no longer URL-decodes ids, so every method addresses the same file.
- **`Location` header:** `PUT` and `PATCH` responses now point at `<collection>/<id>.<ext>`. They used to repeat the id (`<collection>/<id>/<id>.<ext>`).
- **Node REST ids:** the Node `Rest` handler now reads the file id like the fetch handlers do: the last path segment, with its extension stripped, and no minimum length. Short ids such as `abc` work on Node too. A UUID-like _parent_ segment is no longer picked as the id.
- **List pagination (`allowList: true`):** `?page=` now returns the rows of that page; it used to repeat the first page. `limit` must be a positive integer, is capped at 1000, and malformed values are ignored. A list request reads at most 1000 files from storage.
- **Batch delete limit:** a JSON batch-delete body over 1 MiB answers `413` on Node and fetch runtimes, and the client actually receives that response.
