@ayepi/files 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,7 +50,7 @@ This package ships dense, machine-oriented reference docs written for **AI codin
50
50
 
51
51
  - [`ayepi-files.md`](./ayepi-files.md)
52
52
 
53
- They live next to the source in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/files) and are **not** shipped in the npm tarball.
53
+ They ship with this package and also live in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/files).
54
54
 
55
55
  ## License
56
56
 
package/ayepi-files.md ADDED
@@ -0,0 +1,410 @@
1
+ <!--
2
+ ayepi-files.md — reference for `@ayepi/files`, written for coding agents.
3
+
4
+ Copy this file into any project that depends on `@ayepi/files` (e.g. into your repo's
5
+ `docs/` or `.claude/` directory) and reference it from your agents and slash commands.
6
+ It documents the public API, the patterns the package expects, and how it works under the
7
+ hood, with copy-pasteable examples. Keep it in sync with the installed package version.
8
+ -->
9
+
10
+ # `@ayepi/files`
11
+
12
+ A generic, **S3-like**, key-based file store for [`@ayepi/core`](./ayepi-core.md): stream
13
+ bytes in under a key, stream them back out, list by prefix, and hand out **presigned**
14
+ upload/download URLs that expire. The `FileStore` interface is tiny and storage-agnostic —
15
+ the bundled filesystem store (`@ayepi/files/fs`) is the default, and `@ayepi/aws`'s
16
+ `s3Files` (see [ayepi-aws.md](./ayepi-aws.md)) implements the **same** interface, so code
17
+ written against `FileStore` is portable across backends. Everything is **stream-first**:
18
+ `put` takes a `ReadableStream` (or any `FileBody`), `get` returns a `FileObject` you read as
19
+ a stream. Reach for it whenever you need uploads/downloads/blob storage behind one swappable
20
+ contract. `@ayepi/core` is a **peer dependency**:
21
+
22
+ ```sh
23
+ pnpm add @ayepi/files @ayepi/core
24
+ ```
25
+
26
+ The package exposes exactly three import specifiers (per `package.json#exports`):
27
+
28
+ - **`@ayepi/files`** (the `.` entry) — dependency-light: the `FileStore` / `Presigner`
29
+ interfaces, the supporting types, and the `toStream` / `collect` / `transfer` stream
30
+ helpers. No Node imports; safe to reference from shared code.
31
+ - **`@ayepi/files/fs`** — `fsFiles`, the Node filesystem-backed `FileStore`.
32
+ - **`@ayepi/files/server`** — `mountFiles` / `createFilesHandler`, which add presigned
33
+ GET/PUT endpoints in front of a store that can't self-serve (the filesystem one). Node-only.
34
+
35
+ ## Public API
36
+
37
+ ### Types — the storage contract (`@ayepi/files`)
38
+
39
+ ```ts
40
+ /** Metadata about a stored object (no body) — the S3 `HeadObject` shape. */
41
+ interface FileInfo {
42
+ readonly key: string; // the object's key
43
+ readonly size: number; // size in bytes
44
+ readonly contentType?: string; // MIME type, if stored
45
+ readonly etag?: string; // opaque content tag, when the backend supplies one
46
+ readonly modifiedAt: number; // last-modified (ms epoch)
47
+ readonly metadata?: Readonly<Record<string, string>>; // arbitrary user metadata
48
+ }
49
+
50
+ /** Anything you can hand to `put` as the body — a stream is preferred. */
51
+ type FileBody = ReadableStream<Uint8Array> | Uint8Array | Blob | string;
52
+
53
+ /** A stored object's metadata plus lazy accessors for its bytes. */
54
+ interface FileObject {
55
+ readonly info: FileInfo;
56
+ stream(): ReadableStream<Uint8Array>; // the body as a byte stream (read it once)
57
+ bytes(): Promise<Uint8Array>; // read the whole body into memory
58
+ text(): Promise<string>; // read the whole body as a UTF-8 string
59
+ }
60
+
61
+ /** A page of `list` results. */
62
+ interface ListResult {
63
+ readonly files: readonly FileInfo[]; // the objects in this page (metadata only), key-sorted
64
+ readonly cursor?: string; // pass to a follow-up `list({ cursor })`; absent when complete
65
+ }
66
+ ```
67
+
68
+ `PutOptions` (`{ contentType?, metadata? }`) and `ListOptions` (`{ limit?, cursor? }`) carry
69
+ the optional `put` / `list` arguments.
70
+
71
+ ### `FileStore`
72
+
73
+ ```ts
74
+ interface FileStore {
75
+ put(key: string, body: FileBody, opts?: PutOptions): Promise<FileInfo>;
76
+ get(key: string): Promise<FileObject | undefined>;
77
+ head(key: string): Promise<FileInfo | undefined>;
78
+ delete(key: string): Promise<boolean>;
79
+ list(prefix: string, opts?: ListOptions): Promise<ListResult>;
80
+ }
81
+ ```
82
+
83
+ The whole storage surface — S3's core operations, nothing more.
84
+
85
+ - `put(key, body, opts?)` — store `body` under `key`, **overwriting** any existing object;
86
+ resolves the resulting `FileInfo`. `body` is any `FileBody` (stream preferred).
87
+ - `get(key)` — fetch an object (metadata + lazy body) or `undefined` if the key is absent.
88
+ - `head(key)` — fetch just the `FileInfo` (no body) or `undefined` if absent.
89
+ - `delete(key)` — delete an object; resolves `true` if it existed, `false` if it didn't.
90
+ - `list(prefix, opts?)` — list objects whose key **starts with** `prefix`, key-sorted and
91
+ paginated. Pass `opts.limit` to cap the page and feed the returned `cursor` back in via
92
+ `opts.cursor` to continue.
93
+
94
+ ### `Presigner`
95
+
96
+ ```ts
97
+ interface Presigner {
98
+ presignDownload(key: string, opts?: { expiresIn?: number }): Promise<string>;
99
+ presignUpload(key: string, opts?: { expiresIn?: number; contentType?: string }): Promise<string>;
100
+ }
101
+ ```
102
+
103
+ A separate capability from `FileStore` because **not every store can self-serve**. `s3Files`
104
+ (`@ayepi/aws`) implements `Presigner` **natively** (AWS signs the URLs — no server needed);
105
+ the filesystem store gets a `Presigner` from `@ayepi/files/server` (`mountFiles` /
106
+ `createFilesHandler`), which serve the signed GET/PUT routes it returns.
107
+
108
+ - `presignDownload(key, opts?)` — a time-limited URL to GET `key`. `expiresIn` is in seconds.
109
+ - `presignUpload(key, opts?)` — a time-limited URL to PUT `key`. `expiresIn` in seconds;
110
+ `contentType` pins the `Content-Type` the upload records.
111
+
112
+ ### Stream helpers (`@ayepi/files`)
113
+
114
+ ```ts
115
+ function toStream(body: FileBody): ReadableStream<Uint8Array>;
116
+ function collect(stream: ReadableStream<Uint8Array>): Promise<Uint8Array>;
117
+ function transfer(src: FileStore, srcKey: string, dst: FileStore, dstKey: string, opts?: PutOptions): Promise<FileInfo>;
118
+ ```
119
+
120
+ - `toStream(body)` — normalize any `FileBody` into a byte stream. An existing
121
+ `ReadableStream` is returned **by identity**; a `Blob` becomes its `.stream()`; a `string`
122
+ is UTF-8 encoded; a `Uint8Array` is wrapped in a single-chunk stream.
123
+ - `collect(stream)` — read a byte stream fully into one `Uint8Array` (concatenating chunks).
124
+ - `transfer(src, srcKey, dst, dstKey, opts?)` — stream an object from one store to another
125
+ (`src[srcKey] → dst[dstKey]`) **without buffering it all in memory**. Carries the source's
126
+ `contentType`/`metadata` unless `opts` overrides them. **Throws** if `srcKey` is missing.
127
+
128
+ ### `fsFiles(opts)` — the filesystem store (`@ayepi/files/fs`)
129
+
130
+ ```ts
131
+ function fsFiles(opts: FsFilesOptions): FileStore;
132
+
133
+ interface FsFilesOptions {
134
+ readonly dir: string; // root directory (created on demand)
135
+ readonly onError?: (err: unknown, op: string, key: string) => void; // observe an I/O error (also re-thrown)
136
+ }
137
+ ```
138
+
139
+ A Node filesystem-backed `FileStore` rooted at `opts.dir`:
140
+
141
+ - **Objects are files.** The key is a `/`-separated relative path under `dir`; nested keys
142
+ create subdirectories on demand (`mkdir -p` semantics on `put`).
143
+ - **Metadata sidecars.** `contentType`/`metadata` are stored in a small `<file>.ayepi-meta`
144
+ JSON sidecar next to the object — written only when there's metadata to store, and excluded
145
+ from listings. A missing or garbled sidecar is tolerated (yields no extra metadata). Keys
146
+ ending in `.ayepi-meta` are reserved.
147
+ - **Atomic writes.** `put` streams the body to a temp file (`<file>.<random>.tmp`) and
148
+ `rename`s it into place, so a reader never sees a half-written object. `etag` is derived
149
+ from size + mtime (`"<size>-<mtime>"`).
150
+ - **Streaming reads.** `get(...).stream()` is `createReadStream` off disk; `bytes()`/`text()`
151
+ collect that stream.
152
+ - **Prefix listing with cursor pagination.** `list` walks the tree, filters by `prefix`,
153
+ sorts keys, and returns up to `limit` (default **1000**); the `cursor` is the last key of
154
+ the page, and a follow-up `list(prefix, { cursor })` resumes after it. A non-existent root
155
+ lists nothing (empty page) rather than throwing.
156
+ - **Key sanitization.** Keys are rejected (`Error: invalid key`) if empty or if any
157
+ `/`-segment is empty, `.`, or `..` — no traversal, no `a//b`.
158
+ - **`onError` observe-then-rethrow.** On a **real** I/O failure (not a benign "not found"),
159
+ `onError(err, op, key)` is called and the error is then **re-thrown** to the caller — it
160
+ observes, it doesn't swallow. `op` is one of `'put'|'get'|'head'|'delete'|'list'`. A
161
+ missing key surfaces as `undefined`/`false` (not an error), so it does **not** fire
162
+ `onError`. A throwing `onError` is itself ignored so it can't mask the original I/O error.
163
+
164
+ ### `@ayepi/files/server` — presigned serving for a `FileStore`
165
+
166
+ A store like `fsFiles` can't hand out working URLs on its own — it has no HTTP surface. This
167
+ module signs short-lived, HMAC-stamped tokens and serves the matching GET/PUT two ways. The
168
+ token is `base64url(payload).hmac-sha256` carrying the key, op (`get`/`put`), expiry, and
169
+ (for uploads) the pinned content-type — so the URL is **opaque and tamper-evident**, and the
170
+ key never appears in the clear. Verification uses `timingSafeEqual` and rejects expired,
171
+ tampered, wrong-op, or unparseable tokens.
172
+
173
+ ```ts
174
+ interface FilesServerOptions {
175
+ readonly secret: string; // HMAC secret used to sign/verify tokens (server-side only)
176
+ readonly basePath?: string; // URL path the routes live at (default '/_files')
177
+ readonly expiresIn?: number; // default presigned-URL lifetime in seconds (default 900 = 15 min)
178
+ readonly now?: () => number; // clock injection (default Date.now) — for tests
179
+ }
180
+ ```
181
+
182
+ #### `mountFiles(app, store, opts)`
183
+
184
+ ```ts
185
+ function mountFiles(
186
+ app: Server<AnySpec>, store: FileStore, opts: FilesServerOptions,
187
+ ): { handle: MountHandle; presign: Presigner };
188
+ ```
189
+
190
+ Hot-mounts two routes onto a **running** ayepi `Server` (via the same `Server.install` the
191
+ plugin host uses — one call, no edits to your spec) and returns a `Presigner` whose URLs
192
+ point at them:
193
+
194
+ - `ayepiFilesDownload` — `GET ${basePath}?t=…`, a raw `streamOut`. Verifies the token
195
+ (`o === 'get'`), `store.get`s the object (404 if missing), then uses the handler's
196
+ `download(filename, contentType)` to set the object's content-type and `length(size)` to
197
+ set `Content-Length` — which is what makes HTTP **Range / `206`** (resumable downloads)
198
+ work.
199
+ - `ayepiFilesUpload` — `PUT ${basePath}?t=…`, a `streamIn`. Verifies the token
200
+ (`o === 'put'`), streams the request body straight into `store.put` (recording the pinned
201
+ `contentType`), and responds `{ key, size }`.
202
+
203
+ Tear the routes down later with `app.uninstall(handle)`.
204
+
205
+ #### `createFilesHandler(store, opts)`
206
+
207
+ ```ts
208
+ function createFilesHandler(
209
+ store: FileStore, opts: FilesServerOptions,
210
+ ): { fetch: (req: Request) => Promise<Response | undefined>; presign: Presigner };
211
+ ```
212
+
213
+ The same verify-and-stream logic as a plain **`fetch(req)`** — **no spec, no `Server.install`,
214
+ works on any runtime**. It returns `undefined` for requests whose pathname isn't `basePath`,
215
+ so you compose it around your server's `fetch` and fall through on a path miss. GET streams
216
+ the object back with `content-type` + `content-length` headers (403 on a bad/wrong-op token,
217
+ 404 if the object is missing); PUT streams the body into `store.put` and returns
218
+ `{ key, size }` JSON (an absent request body is treated as empty → `size: 0`); any other
219
+ method is `405`.
220
+
221
+ > **Why presign needs server endpoints for the filesystem store:** a `Presigner` URL only
222
+ > works if something serves it. S3 *is* the server, so `s3Files` signs URLs AWS already
223
+ > honors and needs **no** mount. The filesystem store has no HTTP surface of its own — so
224
+ > `mountFiles` / `createFilesHandler` supply the signed GET/PUT endpoints (and the `Presigner`
225
+ > that points at them). Use `@ayepi/aws`'s `s3Files` and you skip this module entirely.
226
+
227
+ ## Examples
228
+
229
+ ### Basic `fsFiles` CRUD
230
+
231
+ ```ts
232
+ import { fsFiles } from '@ayepi/files/fs';
233
+
234
+ const files = fsFiles({ dir: './uploads' });
235
+
236
+ // put accepts a string / Uint8Array / Blob / ReadableStream
237
+ const info = await files.put('docs/a.txt', 'hello', { contentType: 'text/plain', metadata: { owner: 'ada' } });
238
+ info.size; // 5
239
+ info.contentType; // 'text/plain'
240
+
241
+ const obj = await files.get('docs/a.txt');
242
+ await obj?.text(); // 'hello'
243
+ (await files.head('docs/a.txt'))?.size; // 5 (metadata only, no body)
244
+
245
+ await files.delete('docs/a.txt'); // → true; → false if it didn't exist
246
+ await files.get('missing'); // → undefined
247
+ ```
248
+
249
+ ### Streaming a large file in and out (no buffering)
250
+
251
+ ```ts
252
+ import { createReadStream, createWriteStream } from 'node:fs';
253
+ import { Readable, Writable } from 'node:stream';
254
+
255
+ // IN: hand `put` a web ReadableStream — it streams to a temp file, then atomically renames.
256
+ await files.put('big.bin', Readable.toWeb(createReadStream('./big.bin')) as ReadableStream<Uint8Array>, {
257
+ contentType: 'application/octet-stream',
258
+ });
259
+
260
+ // OUT: pipe the object stream straight to a sink — the whole body never sits in memory.
261
+ const out = await files.get('big.bin');
262
+ await out!.stream().pipeTo(Writable.toWeb(createWriteStream('./copy.bin')));
263
+ ```
264
+
265
+ ### Transferring between two stores
266
+
267
+ ```ts
268
+ import { transfer } from '@ayepi/files';
269
+
270
+ const a = fsFiles({ dir: './a' });
271
+ const b = fsFiles({ dir: './b' });
272
+ await a.put('src.txt', 'payload', { contentType: 'text/plain' });
273
+
274
+ // streams src → dst, carrying the source's content-type/metadata
275
+ const info = await transfer(a, 'src.txt', b, 'dst.txt');
276
+ info.key; // 'dst.txt'
277
+ info.contentType; // 'text/plain'
278
+
279
+ // opts override what the source carried
280
+ await transfer(a, 'src.txt', b, 'dst2.txt', { contentType: 'application/json', metadata: { x: '1' } });
281
+
282
+ await transfer(a, 'missing', b, 'x'); // throws: source key "missing" not found
283
+ ```
284
+
285
+ The same call works across backends — e.g. `transfer(fsStore, key, s3Store, key)` to migrate
286
+ a local file into S3.
287
+
288
+ ### Listing by prefix with cursor pagination
289
+
290
+ ```ts
291
+ const all = await files.list('a/'); // every key under 'a/', sorted
292
+ all.files.map((f) => f.key); // ['a/1', 'a/2', 'a/3']
293
+
294
+ const page1 = await files.list('a/', { limit: 2 });
295
+ page1.files.map((f) => f.key); // ['a/1', 'a/2']
296
+ page1.cursor; // 'a/2'
297
+ const page2 = await files.list('a/', { limit: 2, cursor: page1.cursor });
298
+ page2.files.map((f) => f.key); // ['a/3']
299
+ page2.cursor; // undefined → done
300
+ ```
301
+
302
+ ### `mountFiles`: a presigned PUT → GET round-trip through a real server
303
+
304
+ ```ts
305
+ import { server, spec } from '@ayepi/core';
306
+ import { fsFiles } from '@ayepi/files/fs';
307
+ import { mountFiles } from '@ayepi/files/server';
308
+
309
+ const app = server(spec({ endpoints: {} }), []);
310
+ const store = fsFiles({ dir: './uploads' });
311
+ const { presign, handle } = mountFiles(app, store, { secret: process.env.FILES_SECRET! });
312
+ handle.eps.length; // 2 — the GET + PUT routes were installed
313
+
314
+ // upload via a presigned PUT URL
315
+ const putUrl = await presign.presignUpload('a/b.txt', { contentType: 'text/plain' });
316
+ const put = await app.fetch(new Request('http://host' + putUrl, { method: 'PUT', body: 'hello world' }));
317
+ await put.json(); // { key: 'a/b.txt', size: 11 }
318
+
319
+ // download via a presigned GET URL — Content-Length is set, so Range/206 works
320
+ const getUrl = await presign.presignDownload('a/b.txt', { expiresIn: 60 });
321
+ const ranged = await app.fetch(new Request('http://host' + getUrl, { headers: { range: 'bytes=0-4' } }));
322
+ ranged.status; // 206
323
+ ranged.headers.get('content-range'); // 'bytes 0-4/11'
324
+ await ranged.text(); // 'hello'
325
+
326
+ // later: app.uninstall(handle) tears the two routes back down
327
+ ```
328
+
329
+ A bad / expired / wrong-op / tampered token is rejected with **403**; a valid token for a
330
+ missing object is **404**.
331
+
332
+ ### `createFilesHandler`: composed around `app.fetch`
333
+
334
+ ```ts
335
+ import { createFilesHandler } from '@ayepi/files/server';
336
+
337
+ const { fetch: filesFetch, presign } = createFilesHandler(store, { secret, basePath: '/files' });
338
+
339
+ // try the files handler first; it returns undefined for non-`/files` paths, so fall through.
340
+ const handler = async (req: Request): Promise<Response> =>
341
+ (await filesFetch(req)) ?? app.fetch(req);
342
+
343
+ const url = await presign.presignUpload('h.txt');
344
+ await handler(new Request('http://host' + url, { method: 'PUT', body: 'hi' })); // → { key:'h.txt', size:2 }
345
+ await filesFetch(new Request('http://host/somewhere-else')); // → undefined (not ours)
346
+ ```
347
+
348
+ ## How it works under the hood
349
+
350
+ - **One interface, swappable backends.** `FileStore` is the only contract. `fsFiles` and
351
+ `s3Files` implement it identically; `transfer` and any of your own code take `FileStore`
352
+ and don't care which is behind it. `Presigner` is deliberately a *separate* interface
353
+ because presigning is a backend capability, not a universal one.
354
+ - **Stream-first body handling.** `toStream` normalizes every `FileBody` to a byte stream up
355
+ front (passing an existing `ReadableStream` through by identity), so `put` always pipes a
356
+ stream and never materializes large bodies. `fsFiles.put` pipes that stream into a temp
357
+ file via `node:stream/promises` `pipeline`, then `rename`s — the publish is atomic and the
358
+ reader-visible object is always complete.
359
+ - **Sidecar metadata.** Rather than a database, `fsFiles` keeps `contentType`/`metadata` in a
360
+ per-object `.ayepi-meta` JSON file, written only when there's something to store and hidden
361
+ from `list` (`isSidecar` filter). `head`/`get` read it best-effort: a missing or corrupt
362
+ sidecar simply means "no extra metadata."
363
+ - **Listing.** `list` does a recursive `readdir` walk, maps disk paths back to `/`-keys,
364
+ filters by `prefix` and (if paging) `key > cursor`, sorts, and slices to `limit`. The
365
+ cursor is just the last key emitted — stable because the listing is always sorted. A
366
+ missing subtree (`ENOENT`) yields an empty result instead of an error.
367
+ - **Presigned tokens.** `signToken` builds `base64url(json).base64url(hmacSha256(json))`;
368
+ `verifyToken` recomputes the HMAC, compares with `timingSafeEqual` (length-checked first),
369
+ parses the payload, and rejects anything expired (`e <= now`) or unparseable. The handlers
370
+ additionally check the op matches the route (`get` for download, `put` for upload).
371
+ - **Two serving styles, one presigner.** Both `mountFiles` and `createFilesHandler` mint URLs
372
+ with the same `makePresigner` (`${basePath}?t=…`). `mountFiles` serves them through a real
373
+ ayepi `Server` (so the download benefits from the framework's `streamOut` + `length()`
374
+ Range handling); `createFilesHandler` serves them as a bare `Request → Response` for
375
+ runtimes where you'd rather not mount.
376
+
377
+ ## Gotchas / constraints
378
+
379
+ - **`fsFiles` is local — no durability or retries.** It's a single-machine filesystem store:
380
+ no replication, no cross-host sharing, no retry/backoff. It's ideal for dev, tests, and
381
+ single-node deployments; for production blob storage use `@ayepi/aws`'s `s3Files` behind
382
+ the same `FileStore` interface.
383
+ - **Presign tokens are signed, not encrypted.** The HMAC makes the token tamper-evident and
384
+ the key isn't human-obvious (it's base64url'd), but the payload is **not** secret — anyone
385
+ with the URL can decode the key and read/write within the token's lifetime. Treat presigned
386
+ URLs as bearer credentials: keep `expiresIn` short and the `secret` server-side only.
387
+ - **`secret` must match across signing and serving.** The same `secret` (and `basePath`) must
388
+ be configured wherever URLs are signed and wherever they're served, or every token verifies
389
+ as a 403. Rotating the secret invalidates all outstanding URLs.
390
+ - **Keys are sanitized — no traversal.** `fsFiles` rejects empty keys and any `.`/`..`/empty
391
+ segment (and `a//b`), so a key can never escape `dir`. Keys ending in `.ayepi-meta` collide
392
+ with sidecars and are reserved. Plan key schemes accordingly.
393
+ - **`get`/`head`/`delete` don't fire `onError` on a miss.** A non-existent key is normal
394
+ control flow (`undefined` / `false`), not an I/O error. `onError` fires only for real
395
+ failures (e.g. a permission or disk error), and the error is **re-thrown** afterward — it's
396
+ for observability, not for swallowing.
397
+ - **`mountFiles` mutates a running server.** It installs two endpoints at `basePath` via the
398
+ internal `Server.install`; make sure `basePath` doesn't collide with your own routes, and
399
+ remember to `app.uninstall(handle)` if you tear the store down.
400
+ - **A stream body can be read once.** `FileObject.stream()` is a live read — consume it once.
401
+ Call `get` again (or use `bytes()`/`text()`) if you need the body twice.
402
+
403
+ ## See also
404
+
405
+ - [ayepi-aws.md](./ayepi-aws.md) — `s3Files`, the S3-backed `FileStore` that **also**
406
+ implements `Presigner` natively (no `@ayepi/files/server` needed), plus the SQS queue.
407
+ - [ayepi-work.md](./ayepi-work.md) — the work/queue engine, for processing uploaded files
408
+ out of band (e.g. transcode on `put`).
409
+ - [ayepi-core.md](./ayepi-core.md) — `spec` / `endpoint` / `implement` / `server`, the
410
+ `streamIn`/`streamOut` endpoint shapes and `Server.install` that `mountFiles` builds on.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ayepi/files",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Generic S3-like key-based file storage for @ayepi/core — stream-first put/get, prefix listing, presigned upload/download URLs, with a filesystem default and stream/transfer helpers",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -18,7 +18,8 @@
18
18
  "type": "module",
19
19
  "sideEffects": false,
20
20
  "files": [
21
- "dist"
21
+ "dist",
22
+ "ayepi-*.md"
22
23
  ],
23
24
  "exports": {
24
25
  ".": {
@@ -57,7 +58,7 @@
57
58
  "node": ">=18"
58
59
  },
59
60
  "peerDependencies": {
60
- "@ayepi/core": "^0.1.0"
61
+ "@ayepi/core": "^0.2.0"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@vitest/coverage-v8": "^2.1.8",
@@ -65,7 +66,7 @@
65
66
  "tsdown": "^0.12.0",
66
67
  "vitest": "^2.1.8",
67
68
  "zod": "^4.4.3",
68
- "@ayepi/core": "0.1.0"
69
+ "@ayepi/core": "0.2.0"
69
70
  },
70
71
  "keywords": [
71
72
  "ayepi",