@ayepi/files 0.4.1 → 0.5.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/ayepi-files.md CHANGED
@@ -96,7 +96,7 @@ The whole storage surface — S3's core operations, nothing more.
96
96
  ```ts
97
97
  interface Presigner {
98
98
  presignDownload(key: string, opts?: { expiresIn?: number }): Promise<string>;
99
- presignUpload(key: string, opts?: { expiresIn?: number; contentType?: string }): Promise<string>;
99
+ presignUpload(key: string, opts?: { expiresIn?: number; contentType?: string; maxSize?: number; sha256?: string }): Promise<string>;
100
100
  }
101
101
  ```
102
102
 
@@ -107,7 +107,10 @@ the filesystem store gets a `Presigner` from `@ayepi/files/server` (`mountFiles`
107
107
 
108
108
  - `presignDownload(key, opts?)` — a time-limited URL to GET `key`. `expiresIn` is in seconds.
109
109
  - `presignUpload(key, opts?)` — a time-limited URL to PUT `key`. `expiresIn` in seconds;
110
- `contentType` pins the `Content-Type` the upload records.
110
+ `contentType` pins the `Content-Type` the upload records. `maxSize` caps the body in bytes and
111
+ `sha256` (hex, any case) pins the hash the bytes must have; an upload that breaks either is
112
+ refused and nothing is stored. `s3Files` signs `sha256` as S3's `x-amz-checksum-sha256` and
113
+ rejects `maxSize` (a presigned `PUT` cannot cap a body).
111
114
 
112
115
  ### Stream helpers (`@ayepi/files`)
113
116
 
@@ -166,7 +169,7 @@ A Node filesystem-backed `FileStore` rooted at `opts.dir`:
166
169
  A store like `fsFiles` can't hand out working URLs on its own — it has no HTTP surface. This
167
170
  module signs short-lived, HMAC-stamped tokens and serves the matching GET/PUT two ways. The
168
171
  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
172
+ (for uploads) the pinned content-type, size cap and SHA-256 — so the URL is **opaque and tamper-evident**, and the
170
173
  key never appears in the clear. Verification uses `timingSafeEqual` and rejects expired,
171
174
  tampered, wrong-op, or unparseable tokens.
172
175
 
@@ -198,7 +201,11 @@ point at them:
198
201
  work.
199
202
  - `ayepiFilesUpload` — `PUT ${basePath}?t=…`, a `streamIn`. Verifies the token
200
203
  (`o === 'put'`), streams the request body straight into `store.put` (recording the pinned
201
- `contentType`), and responds `{ key, size }`.
204
+ `contentType`), and responds `{ key, size }`. A token's `maxSize` and `sha256` are checked as
205
+ the body streams: a declared `Content-Length` over the cap is refused before reading, a body
206
+ that grows past it errors mid-stream (`413 TOO_LARGE`), and one whose hash differs errors at
207
+ the end (`422 CHECKSUM_MISMATCH`). The error reaches the store as a failed body stream, so
208
+ `fsFiles` never publishes the object and removes its temp file.
202
209
 
203
210
  Tear the routes down later with `app.uninstall(handle)`.
204
211
 
@@ -215,8 +222,9 @@ works on any runtime**. It returns `undefined` for requests whose pathname isn't
215
222
  so you compose it around your server's `fetch` and fall through on a path miss. GET streams
216
223
  the object back with `content-type` + `content-length` headers (403 on a bad/wrong-op token,
217
224
  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`.
225
+ `{ key, size }` JSON (an absent request body is treated as empty → `size: 0`), with the same
226
+ `maxSize`/`sha256` checks answered as `{ error: { code, message } }` with `413`/`422`; any other
227
+ method is `405`. Both styles export the refusal as `UploadRefused` (`status`, `code`).
220
228
 
221
229
  > **Why presign needs server endpoints for the filesystem store:** a `Presigner` URL only
222
230
  > works if something serves it. S3 *is* the server, so `s3Files` signs URLs AWS already
package/dist/fs.cjs CHANGED
@@ -81,7 +81,12 @@ function fsFiles(opts) {
81
81
  return guard("put", key, async () => {
82
82
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
83
83
  const tmp = `${path}.${(0, node_crypto.randomBytes)(8).toString("hex")}.tmp`;
84
- await (0, node_stream_promises.pipeline)(node_stream.Readable.fromWeb(normalize(body)), (0, node_fs.createWriteStream)(tmp));
84
+ try {
85
+ await (0, node_stream_promises.pipeline)(node_stream.Readable.fromWeb(normalize(body)), (0, node_fs.createWriteStream)(tmp));
86
+ } catch (err) {
87
+ await (0, node_fs_promises.unlink)(tmp).catch(() => {});
88
+ throw err;
89
+ }
85
90
  await (0, node_fs_promises.rename)(tmp, path);
86
91
  const meta = {
87
92
  contentType: options?.contentType,
package/dist/fs.js CHANGED
@@ -80,7 +80,12 @@ function fsFiles(opts) {
80
80
  return guard("put", key, async () => {
81
81
  await mkdir(dirname(path), { recursive: true });
82
82
  const tmp = `${path}.${randomBytes(8).toString("hex")}.tmp`;
83
- await pipeline(Readable.fromWeb(normalize(body)), createWriteStream(tmp));
83
+ try {
84
+ await pipeline(Readable.fromWeb(normalize(body)), createWriteStream(tmp));
85
+ } catch (err) {
86
+ await unlink(tmp).catch(() => {});
87
+ throw err;
88
+ }
84
89
  await rename(tmp, path);
85
90
  const meta = {
86
91
  contentType: options?.contentType,
package/dist/index.d.cts CHANGED
@@ -100,6 +100,17 @@ interface PresignUploadOptions {
100
100
  readonly expiresIn?: number;
101
101
  /** Pin the `Content-Type` the upload must use. */
102
102
  readonly contentType?: string;
103
+ /**
104
+ * The most bytes the upload may carry. A larger body is refused (`413 TOO_LARGE`) and nothing is
105
+ * stored. S3's presigned `PUT` cannot cap a body, so `s3Files` throws when this is set.
106
+ */
107
+ readonly maxSize?: number;
108
+ /**
109
+ * The SHA-256 the uploaded bytes must hash to, as lowercase hex. A body that hashes otherwise is
110
+ * refused (`422 CHECKSUM_MISMATCH`) and nothing is stored. `s3Files` signs it as the object's
111
+ * `x-amz-checksum-sha256`, so S3 checks it.
112
+ */
113
+ readonly sha256?: string;
103
114
  }
104
115
  /**
105
116
  * The presign capability — kept separate from {@link FileStore} because not every store can
package/dist/index.d.ts CHANGED
@@ -100,6 +100,17 @@ interface PresignUploadOptions {
100
100
  readonly expiresIn?: number;
101
101
  /** Pin the `Content-Type` the upload must use. */
102
102
  readonly contentType?: string;
103
+ /**
104
+ * The most bytes the upload may carry. A larger body is refused (`413 TOO_LARGE`) and nothing is
105
+ * stored. S3's presigned `PUT` cannot cap a body, so `s3Files` throws when this is set.
106
+ */
107
+ readonly maxSize?: number;
108
+ /**
109
+ * The SHA-256 the uploaded bytes must hash to, as lowercase hex. A body that hashes otherwise is
110
+ * refused (`422 CHECKSUM_MISMATCH`) and nothing is stored. `s3Files` signs it as the object's
111
+ * `x-amz-checksum-sha256`, so S3 checks it.
112
+ */
113
+ readonly sha256?: string;
103
114
  }
104
115
  /**
105
116
  * The presign capability — kept separate from {@link FileStore} because not every store can