@nxgt/s3 0.1.0 → 0.2.1
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 +75 -6
- package/dist/bucket/bind-bucket.d.ts +10 -5
- package/dist/bucket/bind-bucket.d.ts.map +1 -1
- package/dist/bucket/operations/presign.d.ts.map +1 -1
- package/dist/bucket/operations/writes.d.ts +2 -4
- package/dist/bucket/operations/writes.d.ts.map +1 -1
- package/dist/bucket/types.d.ts +22 -1
- package/dist/bucket/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -6
- package/dist/index.js.map +5 -5
- package/docs/README.md +16 -0
- package/docs/guide/buckets.md +170 -0
- package/docs/guide/presigned-urls.md +153 -0
- package/docs/guide/reads.md +186 -0
- package/docs/guide/writes.md +260 -0
- package/docs/roadmap.md +58 -0
- package/docs/troubleshooting.md +276 -0
- package/package.json +2 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
This package throws one error of its own, `S3Error`, with a `code` of
|
|
4
|
+
`WRONG_TYPE`, `TOO_LARGE` or `UNMEASURABLE`, and the object `key` it was
|
|
5
|
+
about — never the body. Every one of them is raised **before** anything is
|
|
6
|
+
sent. The service's own failures, and the checks Bun makes on an option's
|
|
7
|
+
value, come back as Bun raises them; the entries below say which is which.
|
|
8
|
+
The Bun messages were measured on Bun 1.4.2.
|
|
9
|
+
|
|
10
|
+
- **Install and import**
|
|
11
|
+
- [`Cannot find package 'bun'`](#cannot-find-package-bun)
|
|
12
|
+
- [`Cannot find module 'bun' or its corresponding type declarations.`](#cannot-find-module-bun-or-its-corresponding-type-declarations)
|
|
13
|
+
- **Configuration**
|
|
14
|
+
- [`defineBucket: a bucket definition needs a bucket`](#definebucket-a-bucket-definition-needs-a-bucket)
|
|
15
|
+
- [`defineBucket: "avatars" has a maxSize of 0; it is a number of bytes, and must be above zero`](#definebucket-avatars-has-a-maxsize-of-0-it-is-a-number-of-bytes-and-must-be-above-zero)
|
|
16
|
+
- [`defineBucket: "avatars" accepts an empty list of content types, so nothing could ever be written. …`](#definebucket-avatars-accepts-an-empty-list-of-content-types-so-nothing-could-ever-be-written-)
|
|
17
|
+
- **Writes refused before they are sent**
|
|
18
|
+
- [`"avatars" accepts image/png, image/jpeg, not application/pdf`](#avatars-accepts-imagepng-imagejpeg-not-applicationpdf)
|
|
19
|
+
- [``"avatars" accepts image/png, image/jpeg, and this write names no content type. Pass `type` ``](#avatars-accepts-imagepng-imagejpeg-and-this-write-names-no-content-type-pass-type-)
|
|
20
|
+
- [`"avatars" accepts 2097152 bytes at most, and this body is 5242880`](#avatars-accepts-2097152-bytes-at-most-and-this-body-is-5242880)
|
|
21
|
+
- [`"avatars" has a maxSize, and this body's size cannot be known before sending it. …`](#avatars-has-a-maxsize-and-this-bodys-size-cannot-be-known-before-sending-it-)
|
|
22
|
+
- [`storageClass must be one of "STANDARD", "STANDARD_IA", "INTELLIGENT_TIERING", …`](#storageclass-must-be-one-of-standard-standard_ia-intelligent_tiering-)
|
|
23
|
+
- [`acl must be one of "private", "public-read", "public-read-write", …`](#acl-must-be-one-of-private-public-read-public-read-write-)
|
|
24
|
+
- **The service**
|
|
25
|
+
- [`Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required`](#missing-s3-credentials-accesskeyid-secretaccesskey-bucket-and-endpoint-are-required)
|
|
26
|
+
- [`The AWS Access Key Id you provided does not exist in our records.`](#the-aws-access-key-id-you-provided-does-not-exist-in-our-records)
|
|
27
|
+
- [A presigned upload stored a body the bucket would have refused](#a-presigned-upload-stored-a-body-the-bucket-would-have-refused)
|
|
28
|
+
- [A listing came back short with a cursor still set](#a-listing-came-back-short-with-a-cursor-still-set)
|
|
29
|
+
|
|
30
|
+
## Install and import
|
|
31
|
+
|
|
32
|
+
### `Cannot find package 'bun'`
|
|
33
|
+
|
|
34
|
+
**When:** importing `@nxgt/s3` under Node — the full line names the file it
|
|
35
|
+
was imported from.
|
|
36
|
+
**Why:** the client is Bun's own `S3Client`, which is why there is no AWS SDK
|
|
37
|
+
to install and why this package does not run on Node. Measured on Node 22.
|
|
38
|
+
**Fix:**
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bun run ./src/index.ts # Bun 1.4 or later
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `Cannot find module 'bun' or its corresponding type declarations.`
|
|
45
|
+
|
|
46
|
+
**When:** typechecking, on the first file that imports `@nxgt/s3`.
|
|
47
|
+
**Why:** the shipped declarations import `S3Client`, `S3File` and `S3Options`
|
|
48
|
+
from `bun`, so your project needs Bun's types. They are not a dependency of
|
|
49
|
+
this package.
|
|
50
|
+
**Fix:**
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
bun add -d @types/bun
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
### `defineBucket: a bucket definition needs a bucket`
|
|
59
|
+
|
|
60
|
+
**When:** at `defineBucket`, with an empty `bucket`.
|
|
61
|
+
**Why:** the bucket name is what every key is written under; an empty one
|
|
62
|
+
cannot be meant.
|
|
63
|
+
**Fix:**
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export const avatars = defineBucket({
|
|
67
|
+
bucket: 'avatars',
|
|
68
|
+
key: (p: { userId: string }) => `${p.userId}.png`,
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `defineBucket: "avatars" has a maxSize of 0; it is a number of bytes, and must be above zero`
|
|
73
|
+
|
|
74
|
+
**When:** at `defineBucket`.
|
|
75
|
+
**Why:** `maxSize` is a number of **bytes**, and it is inclusive: 1024 passes
|
|
76
|
+
and 1025 does not. A string body is measured in bytes, not in characters.
|
|
77
|
+
**Fix:**
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
defineBucket({ bucket: 'avatars', key, maxSize: 2 * 1024 * 1024 });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `defineBucket: "avatars" accepts an empty list of content types, so nothing could ever be written. …`
|
|
84
|
+
|
|
85
|
+
**When:** at `defineBucket`, with `contentType: []`.
|
|
86
|
+
**Why:** an empty list refuses every write. The message ends with the way out,
|
|
87
|
+
``Leave `contentType` out to accept anything``: leaving the option out is what
|
|
88
|
+
accepts anything, and `[]` is never what was meant.
|
|
89
|
+
**Fix:**
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
defineBucket({ bucket: 'avatars', key, contentType: ['image/png', 'image/jpeg'] });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Writes refused before they are sent
|
|
96
|
+
|
|
97
|
+
### `"avatars" accepts image/png, image/jpeg, not application/pdf`
|
|
98
|
+
|
|
99
|
+
**When:** `put` or `presignGet`, with a `type` the definition does not list.
|
|
100
|
+
**Why:** an `S3Error` with `code: 'WRONG_TYPE'`. Nothing was sent. The type
|
|
101
|
+
is compared on its **essence**: `text/csv` accepts `text/csv;charset=utf-8`
|
|
102
|
+
and `TEXT/CSV`, because that is what real bodies carry — parameters and case
|
|
103
|
+
are ignored, nothing else is.
|
|
104
|
+
**Fix:**
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { S3Error } from '@nxgt/s3';
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await avatars.put({ userId }, body, { type: file.type });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof S3Error && error.code === 'WRONG_TYPE') return badRequest();
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### ``"avatars" accepts image/png, image/jpeg, and this write names no content type. Pass `type` ``
|
|
118
|
+
|
|
119
|
+
**When:** `put` on a bucket with `contentType`, for a body that carries no
|
|
120
|
+
type of its own — a string, a typed array, a stream.
|
|
121
|
+
**Why:** the guard cannot accept what it cannot read, so an unnamed type is
|
|
122
|
+
refused rather than guessed. `code: 'WRONG_TYPE'`.
|
|
123
|
+
**Fix:**
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await avatars.put({ userId }, bytes, { type: 'image/png' });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`Bun.file(path)` carries its own type, and does not need the option.
|
|
130
|
+
|
|
131
|
+
### `"avatars" accepts 2097152 bytes at most, and this body is 5242880`
|
|
132
|
+
|
|
133
|
+
**When:** `put`, for a body whose size is known and above `maxSize`.
|
|
134
|
+
**Why:** an `S3Error` with `code: 'TOO_LARGE'`, raised before the request goes
|
|
135
|
+
out.
|
|
136
|
+
**Fix:**
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
if (error instanceof S3Error && error.code === 'TOO_LARGE') return payloadTooLarge();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `"avatars" has a maxSize, and this body's size cannot be known before sending it. …`
|
|
143
|
+
|
|
144
|
+
**When:** `put` with `maxSize` set, for a `Response`, a `Request`, a stream or
|
|
145
|
+
another `S3File`.
|
|
146
|
+
**Why:** nothing can check a length it has not read — an `S3File` reports its
|
|
147
|
+
size as `NaN` until the service has been asked — so the write is refused
|
|
148
|
+
rather than streamed unchecked. `code: 'UNMEASURABLE'`. The message ends with
|
|
149
|
+
the two ways out, ``Read it into memory first, or drop `maxSize` and let the
|
|
150
|
+
service refuse it``.
|
|
151
|
+
**Fix:**
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await avatars.put({ userId }, await response.bytes()); // read it in first
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or drop `maxSize` from the definition and let the service refuse an oversized
|
|
158
|
+
body.
|
|
159
|
+
|
|
160
|
+
### `storageClass must be one of "STANDARD", "STANDARD_IA", "INTELLIGENT_TIERING", …`
|
|
161
|
+
|
|
162
|
+
**When:** `put` or `presignPut` with a `storageClass` Bun does not know.
|
|
163
|
+
**Why:** an option's **value** is Bun's to check, and it throws **Bun's own
|
|
164
|
+
`TypeError`** — not an `S3Error`. `instanceof S3Error` is false, and
|
|
165
|
+
`error.code` is not one of this package's. Nothing is sent either way; it is
|
|
166
|
+
the class a handler catches that differs.
|
|
167
|
+
**Fix:**
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import type { PutOptions } from '@nxgt/s3';
|
|
171
|
+
|
|
172
|
+
// the classes this application allows, proved against Bun's own list
|
|
173
|
+
const CLASSES = ['STANDARD', 'STANDARD_IA'] as const satisfies readonly NonNullable<
|
|
174
|
+
PutOptions['storageClass']
|
|
175
|
+
>[];
|
|
176
|
+
|
|
177
|
+
function storageClassOf(value: string | undefined): PutOptions['storageClass'] {
|
|
178
|
+
return CLASSES.find((known) => known === value); // undefined: the bucket's default
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await avatars.put({ userId }, bytes, { storageClass: storageClassOf(body.storageClass) });
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Catch `TypeError` beside `S3Error` where such a value can reach a call.
|
|
185
|
+
|
|
186
|
+
### `acl must be one of "private", "public-read", "public-read-write", …`
|
|
187
|
+
|
|
188
|
+
**When:** `put`, `presignGet` or `presignPut` with an `acl` Bun does not
|
|
189
|
+
know.
|
|
190
|
+
**Why:** the same as `storageClass`: Bun's own `TypeError`, before anything is
|
|
191
|
+
sent. `contentDisposition` and `contentEncoding`, by contrast, are plain
|
|
192
|
+
strings to Bun and accept anything.
|
|
193
|
+
**Fix:**
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
await avatars.put({ userId }, bytes, { acl: 'public-read' });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## The service
|
|
200
|
+
|
|
201
|
+
### `Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required`
|
|
202
|
+
|
|
203
|
+
**When:** the first call, when neither `bindBucket`'s options nor the
|
|
204
|
+
environment gave the client credentials. Bun's error, `code:
|
|
205
|
+
'ERR_S3_MISSING_CREDENTIALS'`.
|
|
206
|
+
**Why:** `bindBucket` creates the `S3Client` for you and passes your options
|
|
207
|
+
through; with none, Bun falls back to the environment, and to AWS's endpoint.
|
|
208
|
+
**Fix:**
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
export const avatars = bindBucket(avatarsDefinition, {
|
|
212
|
+
endpoint: process.env.S3_ENDPOINT,
|
|
213
|
+
accessKeyId: process.env.S3_KEY,
|
|
214
|
+
secretAccessKey: process.env.S3_SECRET,
|
|
215
|
+
virtualHostedStyle: false, // for a service that is not AWS
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `The AWS Access Key Id you provided does not exist in our records.`
|
|
220
|
+
|
|
221
|
+
**When:** the first call, with credentials the service refuses — or with no
|
|
222
|
+
`endpoint`, which sends the request to AWS whatever your service is.
|
|
223
|
+
**Why:** it is the **service's** answer, raised by Bun as an error whose
|
|
224
|
+
`name` is `S3Error` and whose `code` is the S3 code (`InvalidAccessKeyId`,
|
|
225
|
+
`SignatureDoesNotMatch`, `NoSuchBucket`, `AccessDenied`). It is **not** this
|
|
226
|
+
package's `S3Error` class: `instanceof S3Error` is false for it.
|
|
227
|
+
**Fix:**
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
try {
|
|
231
|
+
await avatars.put({ userId }, bytes);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof S3Error) return badRequest(error.code); // ours: the guards
|
|
234
|
+
throw error; // the service's, or Bun's: log the code it carries
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`bytes`, `text` and `stat` are the exception: they turn S3's own `NoSuchKey`
|
|
239
|
+
into `undefined`, so `undefined` means "no such object" and nothing else.
|
|
240
|
+
Every other failure comes back as the error it is.
|
|
241
|
+
|
|
242
|
+
### A presigned upload stored a body the bucket would have refused
|
|
243
|
+
|
|
244
|
+
**When:** after handing out a `presignPut` URL. No error anywhere.
|
|
245
|
+
**Why:** a presigned PUT constrains the key and the deadline, and nothing
|
|
246
|
+
else. Measured on Bun 1.4: `X-Amz-SignedHeaders` stays `host`, so the
|
|
247
|
+
uploader's `Content-Type` is never signed and the size is never checked —
|
|
248
|
+
which is why `presignPut` takes no `type` at all. The guards are `put`'s;
|
|
249
|
+
`file(params).writer()` and `client` are Bun's own and write whatever they
|
|
250
|
+
are given.
|
|
251
|
+
**Fix:**
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const url = avatars.presignPut({ userId }, { expiresIn: 300 });
|
|
255
|
+
// after the upload, check what actually landed
|
|
256
|
+
const stat = await avatars.stat({ userId });
|
|
257
|
+
if (!stat || stat.size > maxSize) await avatars.delete({ userId });
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Set the service's own bucket policy too where it matters.
|
|
261
|
+
|
|
262
|
+
### A listing came back short with a cursor still set
|
|
263
|
+
|
|
264
|
+
**When:** `list`, on an eventually consistent service.
|
|
265
|
+
**Why:** `limit` is a maximum, not a promise.
|
|
266
|
+
**Fix:**
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
let cursor: string | null = null;
|
|
270
|
+
do {
|
|
271
|
+
const page = await avatars.list({ prefix, limit: 100, cursor });
|
|
272
|
+
cursor = page.nextCursor;
|
|
273
|
+
} while (cursor !== null);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Page until `nextCursor` is `null`, never until a page is short.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/s3",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "S3 on Bun's own client: buckets described once, keys built by a typed function, uploads refused before they are sent, and presigned URLs from the same definition",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"docs",
|
|
11
12
|
"README.md",
|
|
12
13
|
"package.json",
|
|
13
14
|
"LICENSE"
|