@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.
@@ -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.0",
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"