@ayepi/files 0.4.0 → 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 +14 -6
- package/dist/fs.cjs +6 -1
- package/dist/fs.js +6 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/server.cjs +2390 -1134
- package/dist/server.d.cts +9 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.js +2391 -1136
- package/package.json +4 -4
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`)
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|