@nxgt/s3 0.2.1 → 0.3.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 +15 -10
- package/dist/bucket/guards.d.ts +6 -0
- package/dist/bucket/guards.d.ts.map +1 -1
- package/dist/bucket/operations/presign.d.ts.map +1 -1
- package/dist/bucket/operations/writes.d.ts.map +1 -1
- package/dist/errors/s3-error.d.ts +6 -1
- package/dist/errors/s3-error.d.ts.map +1 -1
- package/dist/index.js +108 -52
- package/dist/index.js.map +7 -7
- package/docs/guide/presigned-urls.md +28 -1
- package/docs/guide/writes.md +27 -14
- package/docs/roadmap.md +6 -0
- package/docs/troubleshooting.md +77 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,6 +174,7 @@ if (error instanceof S3Error && error.code === 'TOO_LARGE') {
|
|
|
174
174
|
| `WRONG_TYPE` | the body's content type is not one this bucket accepts — or the write named none and the bucket names some |
|
|
175
175
|
| `TOO_LARGE` | the body is bigger than `maxSize` |
|
|
176
176
|
| `UNMEASURABLE` | `maxSize` is set and the body's size cannot be known before sending |
|
|
177
|
+
| `WRONG_OPTION` | an option's own value is not one the service accepts: `acl` or `storageClass` on a write, `acl` or `expiresIn` on a presigned URL |
|
|
177
178
|
|
|
178
179
|
`defineBucket` throws a `TypeError` for a definition that could never work: an
|
|
179
180
|
empty `bucket`, a `maxSize` that is not a positive number, an empty list of
|
|
@@ -242,18 +243,22 @@ Each is a `@ts-expect-error` case in `test/types/s3.ts`.
|
|
|
242
243
|
set and one without come back with the **same** ETag, and neither carries
|
|
243
244
|
the `-<parts>` suffix a multipart upload leaves. Use `file(params).writer()`
|
|
244
245
|
for a body that wants parts.
|
|
245
|
-
- **
|
|
246
|
-
|
|
247
|
-
`
|
|
248
|
-
|
|
249
|
-
`
|
|
250
|
-
`
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
- **A wrong `acl` or `storageClass` is `WRONG_OPTION`, not Bun's `TypeError`.**
|
|
247
|
+
Both values are checked here, with the content type and the size — and an
|
|
248
|
+
`acl` on a presigned URL against the same list — so one `catch` takes every
|
|
249
|
+
refusal:
|
|
250
|
+
`if (error instanceof S3Error) return c.json({ error: error.code }, 400);`.
|
|
251
|
+
`contentDisposition` and `contentEncoding` are free strings and are never
|
|
252
|
+
refused. `presignGet` and `presignPut` forward `acl` too, and go through the
|
|
253
|
+
same allowlist. The accepted values are listed in
|
|
254
|
+
[docs/guide/writes.md](docs/guide/writes.md).
|
|
254
255
|
- **A string body is measured in bytes, not in characters**, and `maxSize` is
|
|
255
256
|
inclusive: 1024 passes, 1025 does not.
|
|
256
|
-
- **`expiresIn` is seconds, and Bun's default is a day.** Always pass one.
|
|
257
|
+
- **`expiresIn` is seconds, and Bun's default is a day.** Always pass one. An
|
|
258
|
+
`S3Error` with `code: 'WRONG_OPTION'` refuses it above 604 800 seconds —
|
|
259
|
+
seven days, S3's own cap on a presigned URL — at or below zero, and for
|
|
260
|
+
anything that is not a finite number, so a signed URL this package hands
|
|
261
|
+
back is one the service will accept.
|
|
257
262
|
- **A key is built, never guessed.** `keyFor` is there so a caller that needs
|
|
258
263
|
the string gets *the* string; building one by hand somewhere else is how a
|
|
259
264
|
bucket ends up with two spellings of the same object.
|
package/dist/bucket/guards.d.ts
CHANGED
|
@@ -22,4 +22,10 @@ export declare function sizeOf(body: PutBody): number | undefined;
|
|
|
22
22
|
export declare function checkType<P>(context: BucketContext<P>, key: string, type: string | undefined): void;
|
|
23
23
|
/** Refuses a body the bucket does not accept, before anything is sent. */
|
|
24
24
|
export declare function checkSize<P>(context: BucketContext<P>, key: string, body: PutBody): void;
|
|
25
|
+
/**
|
|
26
|
+
* Refuses a value the service does not accept for `acl` or `storageClass`.
|
|
27
|
+
* Shared by `put` and `presign`, so the same wrong `acl` is the same error
|
|
28
|
+
* whichever one a caller reached for. An option with no fixed set passes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkOption(key: string, name: string, value: unknown): void;
|
|
25
31
|
//# sourceMappingURL=guards.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/bucket/guards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/bucket/guards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,KAAK,EAAE,OAAO,EAAc,MAAM,SAAS,CAAC;AAEnD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC5B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,GACvB,MAAM,GAAG,SAAS,CAGpB;AAED,8EAA8E;AAC9E,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAaxD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,CAAC,EAC1B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GAAG,SAAS,GACtB,IAAI,CAmBN;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,CAAC,EAC1B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,OAAO,GACX,IAAI,CAoBN;AA4ED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAiB3E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presign.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/presign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"presign.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/presign.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAGvD,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC9B,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;CACvB;AAqCD,wBAAgB,aAAa,CAAC,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE,cAAc,GACtB,MAAM,CAMR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE,cAAc,GACtB,MAAM,CAMR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writes.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/writes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAEvD,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEpD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAChC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,UAAe,GACtB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"writes.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/writes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAEvD,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEpD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAChC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,UAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CAef;AA4CD,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,CAAC,EAC7B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,IAAI,CAAC,CAEf"}
|
|
@@ -5,7 +5,12 @@ export type S3ErrorCode =
|
|
|
5
5
|
/** The body is bigger than this bucket's `maxSize`. */
|
|
6
6
|
| 'TOO_LARGE'
|
|
7
7
|
/** The body's size cannot be known before sending, and `maxSize` is set. */
|
|
8
|
-
| 'UNMEASURABLE'
|
|
8
|
+
| 'UNMEASURABLE'
|
|
9
|
+
/**
|
|
10
|
+
* An option's own value is not one the service accepts: `acl` or
|
|
11
|
+
* `storageClass` on a write, `acl` or `expiresIn` on a presigned URL.
|
|
12
|
+
*/
|
|
13
|
+
| 'WRONG_OPTION';
|
|
9
14
|
/**
|
|
10
15
|
* This package's only error, and every one of them is thrown **before**
|
|
11
16
|
* anything is sent. S3's own failures come back as they are, from Bun's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3-error.d.ts","sourceRoot":"","sources":["../../src/errors/s3-error.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,MAAM,MAAM,WAAW;AACtB,8DAA8D;AAC5D,YAAY;AACd,uDAAuD;GACrD,WAAW;AACb,4EAA4E;GAC1E,cAAc,CAAC;AAElB;;;;GAIG;AACH,qBAAa,OAAQ,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBAET,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM3D"}
|
|
1
|
+
{"version":3,"file":"s3-error.d.ts","sourceRoot":"","sources":["../../src/errors/s3-error.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,MAAM,MAAM,WAAW;AACtB,8DAA8D;AAC5D,YAAY;AACd,uDAAuD;GACrD,WAAW;AACb,4EAA4E;GAC1E,cAAc;AAChB;;;GAGG;GACD,cAAc,CAAC;AAElB;;;;GAIG;AACH,qBAAa,OAAQ,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBAET,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM3D"}
|
package/dist/index.js
CHANGED
|
@@ -36,52 +36,6 @@ async function listObjects(context, options = {}) {
|
|
|
36
36
|
return { items, nextCursor: next };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// src/bucket/operations/presign.ts
|
|
40
|
-
var SIGNED = ["expiresIn", "acl"];
|
|
41
|
-
function signed(options) {
|
|
42
|
-
const forwarded = {};
|
|
43
|
-
for (const key of SIGNED) {
|
|
44
|
-
if (options?.[key] !== undefined)
|
|
45
|
-
forwarded[key] = options[key];
|
|
46
|
-
}
|
|
47
|
-
return forwarded;
|
|
48
|
-
}
|
|
49
|
-
function presignGetUrl(context, params, options) {
|
|
50
|
-
return context.client.presign(keyOf(context, params), {
|
|
51
|
-
...signed(options),
|
|
52
|
-
method: "GET"
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function presignPutUrl(context, params, options) {
|
|
56
|
-
return context.client.presign(keyOf(context, params), {
|
|
57
|
-
...signed(options),
|
|
58
|
-
method: "PUT"
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// src/bucket/operations/reads.ts
|
|
63
|
-
async function whenPresent(context, params, read) {
|
|
64
|
-
try {
|
|
65
|
-
return await read(context.client.file(keyOf(context, params)));
|
|
66
|
-
} catch (reason) {
|
|
67
|
-
if (reason.code === "NoSuchKey")
|
|
68
|
-
return;
|
|
69
|
-
throw reason;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function readBytes(context, params) {
|
|
73
|
-
return whenPresent(context, params, (file) => file.bytes());
|
|
74
|
-
}
|
|
75
|
-
function readText(context, params) {
|
|
76
|
-
return whenPresent(context, params, (file) => file.text());
|
|
77
|
-
}
|
|
78
|
-
function statObject(context, params) {
|
|
79
|
-
return whenPresent(context, params, (file) => file.stat());
|
|
80
|
-
}
|
|
81
|
-
function objectExists(context, params) {
|
|
82
|
-
return context.client.exists(keyOf(context, params));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
39
|
// src/errors/s3-error.ts
|
|
86
40
|
class S3Error extends Error {
|
|
87
41
|
constructor(code, key, message) {
|
|
@@ -135,6 +89,104 @@ function checkSize(context, key, body) {
|
|
|
135
89
|
throw new S3Error("TOO_LARGE", key, `"${bucket}" accepts ${maxSize} bytes at most, and this body is ${size}`);
|
|
136
90
|
}
|
|
137
91
|
}
|
|
92
|
+
var ACLS = [
|
|
93
|
+
"private",
|
|
94
|
+
"public-read",
|
|
95
|
+
"public-read-write",
|
|
96
|
+
"aws-exec-read",
|
|
97
|
+
"authenticated-read",
|
|
98
|
+
"bucket-owner-read",
|
|
99
|
+
"bucket-owner-full-control",
|
|
100
|
+
"log-delivery-write"
|
|
101
|
+
];
|
|
102
|
+
var STORAGE_CLASSES = [
|
|
103
|
+
"STANDARD",
|
|
104
|
+
"DEEP_ARCHIVE",
|
|
105
|
+
"EXPRESS_ONEZONE",
|
|
106
|
+
"GLACIER",
|
|
107
|
+
"GLACIER_IR",
|
|
108
|
+
"INTELLIGENT_TIERING",
|
|
109
|
+
"ONEZONE_IA",
|
|
110
|
+
"OUTPOSTS",
|
|
111
|
+
"REDUCED_REDUNDANCY",
|
|
112
|
+
"SNOW",
|
|
113
|
+
"STANDARD_IA"
|
|
114
|
+
];
|
|
115
|
+
var ALLOWED = {
|
|
116
|
+
acl: ACLS,
|
|
117
|
+
storageClass: STORAGE_CLASSES
|
|
118
|
+
};
|
|
119
|
+
var MAX_EXPIRES_IN = 604800;
|
|
120
|
+
function checkOption(key, name, value) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
return;
|
|
123
|
+
if (name === "expiresIn") {
|
|
124
|
+
checkExpiresIn(key, value);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const allowed = ALLOWED[name];
|
|
128
|
+
if (!allowed)
|
|
129
|
+
return;
|
|
130
|
+
if (!allowed.includes(value)) {
|
|
131
|
+
throw new S3Error("WRONG_OPTION", key, `${name} must be one of ${allowed.join(", ")}; got ${JSON.stringify(value)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function checkExpiresIn(key, value) {
|
|
135
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > MAX_EXPIRES_IN) {
|
|
136
|
+
throw new S3Error("WRONG_OPTION", key, `expiresIn is seconds, and must be above 0 and at most ` + `${MAX_EXPIRES_IN} (seven days, which is S3's own limit); ` + `got ${JSON.stringify(value)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/bucket/operations/presign.ts
|
|
141
|
+
var SIGNED = ["expiresIn", "acl"];
|
|
142
|
+
function signed(key, options) {
|
|
143
|
+
const forwarded = {};
|
|
144
|
+
for (const name of SIGNED) {
|
|
145
|
+
const value = options?.[name];
|
|
146
|
+
if (value === undefined)
|
|
147
|
+
continue;
|
|
148
|
+
checkOption(key, name, value);
|
|
149
|
+
forwarded[name] = value;
|
|
150
|
+
}
|
|
151
|
+
return forwarded;
|
|
152
|
+
}
|
|
153
|
+
function presignGetUrl(context, params, options) {
|
|
154
|
+
const key = keyOf(context, params);
|
|
155
|
+
return context.client.presign(key, {
|
|
156
|
+
...signed(key, options),
|
|
157
|
+
method: "GET"
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function presignPutUrl(context, params, options) {
|
|
161
|
+
const key = keyOf(context, params);
|
|
162
|
+
return context.client.presign(key, {
|
|
163
|
+
...signed(key, options),
|
|
164
|
+
method: "PUT"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/bucket/operations/reads.ts
|
|
169
|
+
async function whenPresent(context, params, read) {
|
|
170
|
+
try {
|
|
171
|
+
return await read(context.client.file(keyOf(context, params)));
|
|
172
|
+
} catch (reason) {
|
|
173
|
+
if (reason.code === "NoSuchKey")
|
|
174
|
+
return;
|
|
175
|
+
throw reason;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function readBytes(context, params) {
|
|
179
|
+
return whenPresent(context, params, (file) => file.bytes());
|
|
180
|
+
}
|
|
181
|
+
function readText(context, params) {
|
|
182
|
+
return whenPresent(context, params, (file) => file.text());
|
|
183
|
+
}
|
|
184
|
+
function statObject(context, params) {
|
|
185
|
+
return whenPresent(context, params, (file) => file.stat());
|
|
186
|
+
}
|
|
187
|
+
function objectExists(context, params) {
|
|
188
|
+
return context.client.exists(keyOf(context, params));
|
|
189
|
+
}
|
|
138
190
|
|
|
139
191
|
// src/bucket/operations/writes.ts
|
|
140
192
|
async function putObject(context, params, body, options = {}) {
|
|
@@ -142,8 +194,9 @@ async function putObject(context, params, body, options = {}) {
|
|
|
142
194
|
const type = effectiveType(body, options.type);
|
|
143
195
|
checkType(context, key, type);
|
|
144
196
|
checkSize(context, key, body);
|
|
197
|
+
const forwarded = passed(key, options);
|
|
145
198
|
await context.client.write(key, body, {
|
|
146
|
-
...
|
|
199
|
+
...forwarded,
|
|
147
200
|
...type ? { type } : {}
|
|
148
201
|
});
|
|
149
202
|
}
|
|
@@ -153,11 +206,14 @@ var PASSED = [
|
|
|
153
206
|
"contentDisposition",
|
|
154
207
|
"contentEncoding"
|
|
155
208
|
];
|
|
156
|
-
function passed(options) {
|
|
209
|
+
function passed(key, options) {
|
|
157
210
|
const forwarded = {};
|
|
158
|
-
for (const
|
|
159
|
-
|
|
160
|
-
|
|
211
|
+
for (const name of PASSED) {
|
|
212
|
+
const value = options[name];
|
|
213
|
+
if (value === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
checkOption(key, name, value);
|
|
216
|
+
forwarded[name] = value;
|
|
161
217
|
}
|
|
162
218
|
return forwarded;
|
|
163
219
|
}
|
|
@@ -207,5 +263,5 @@ export {
|
|
|
207
263
|
defineBucket
|
|
208
264
|
};
|
|
209
265
|
|
|
210
|
-
//# debugId=
|
|
266
|
+
//# debugId=B73383C85513ABE564756E2164756E21
|
|
211
267
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/bucket/bind-bucket.ts", "../src/bucket/context.ts", "../src/bucket/operations/list.ts", "../src/
|
|
3
|
+
"sources": ["../src/bucket/bind-bucket.ts", "../src/bucket/context.ts", "../src/bucket/operations/list.ts", "../src/errors/s3-error.ts", "../src/bucket/guards.ts", "../src/bucket/operations/presign.ts", "../src/bucket/operations/reads.ts", "../src/bucket/operations/writes.ts", "../src/bucket/define-bucket.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { S3Client, type S3File, type S3Options, type S3Stats } from 'bun';\nimport { bucketContext, keyOf } from './context';\nimport { listObjects } from './operations/list';\nimport {\n\ttype PresignOptions,\n\tpresignGetUrl,\n\tpresignPutUrl,\n} from './operations/presign';\nimport {\n\tobjectExists,\n\treadBytes,\n\treadText,\n\tstatObject,\n} from './operations/reads';\nimport { deleteObject, putObject } from './operations/writes';\nimport type {\n\tBucketDefinition,\n\tObjectPage,\n\tPutBody,\n\tPutOptions,\n} from './types';\n\nexport type { PresignOptions };\n\n/** A bucket bound to credentials: the definition, with somewhere to put it. */\nexport interface BoundBucket<P> {\n\t/** The client this holds, for anything this package does not wrap. */\n\treadonly client: S3Client;\n\t/** The key this would use, for a caller that needs the string itself. */\n\tkeyFor(params: P): string;\n\t/** Bun's own lazy handle: `.stream()`, `.slice()`, `.writer()` and the rest. */\n\tfile(params: P): S3File;\n\t/**\n\t * Writes it, once the bucket's content type and size have accepted it.\n\t *\n\t * Beyond `type`, an option here describes **this object** — how it is\n\t * served back (`contentDisposition`, `contentEncoding`), who may read it\n\t * (`acl`), what it costs to keep (`storageClass`) — or how a large body is\n\t * uploaded (`partSize`, `queueSize`, `retry`).\n\t */\n\tput(params: P, body: PutBody, options?: PutOptions): Promise<void>;\n\t/** The bytes, or `undefined` when there is no such object. */\n\tbytes(params: P): Promise<Uint8Array | undefined>;\n\t/** The body as text, or `undefined` when there is no such object. */\n\ttext(params: P): Promise<string | undefined>;\n\texists(params: P): Promise<boolean>;\n\t/** What the service knows about it, or `undefined` when it is not there. */\n\tstat(params: P): Promise<S3Stats | undefined>;\n\t/** Removes it. S3 does not say whether anything was there, and nor does this. */\n\tdelete(params: P): Promise<void>;\n\t/** One page of the bucket, in this repository's cursor shape. */\n\tlist(options?: {\n\t\tprefix?: string;\n\t\tlimit?: number;\n\t\tcursor?: string | null;\n\t}): Promise<ObjectPage>;\n\t/** A URL that reads this object, signed. */\n\tpresignGet(params: P, options?: PresignOptions): string;\n\t/**\n\t * A URL that writes this object, signed. It constrains the key and the\n\t * deadline, and **nothing else** — not the size, not the content type.\n\t */\n\tpresignPut(params: P, options?: PresignOptions): string;\n}\n\n/**\n * Binds a bucket definition to credentials.\n *\n * ```ts\n * const store = bindBucket(avatars, {\n * \tendpoint: process.env.S3_ENDPOINT,\n * \taccessKeyId: process.env.S3_KEY,\n * \tsecretAccessKey: process.env.S3_SECRET,\n * });\n * await store.put({ userId: 'u1' }, png, { type: 'image/png' });\n * ```\n *\n * Given no options, Bun reads its own `S3_*` / `AWS_*` environment variables.\n *\n * Each bound bucket holds an `S3Client` of its own. S3 is stateless HTTP —\n * there is no connection to share, and nothing to close.\n */\nexport function bindBucket<P>(\n\tdefinition: BucketDefinition<P>,\n\toptions: Omit<S3Options, 'bucket'> = {},\n): BoundBucket<P> {\n\tconst client = new S3Client({ ...options, bucket: definition.bucket });\n\tconst context = bucketContext(client, definition);\n\treturn {\n\t\tclient,\n\t\tkeyFor: (params) => keyOf(context, params),\n\t\tfile: (params) => client.file(keyOf(context, params)),\n\t\tput: (params, body, putOptions) =>\n\t\t\tputObject(context, params, body, putOptions),\n\t\tbytes: (params) => readBytes(context, params),\n\t\ttext: (params) => readText(context, params),\n\t\texists: (params) => objectExists(context, params),\n\t\tstat: (params) => statObject(context, params),\n\t\tdelete: (params) => deleteObject(context, params),\n\t\tlist: (listOptions) => listObjects(context, listOptions),\n\t\tpresignGet: (params, presignOptions) =>\n\t\t\tpresignGetUrl(context, params, presignOptions),\n\t\tpresignPut: (params, presignOptions) =>\n\t\t\tpresignPutUrl(context, params, presignOptions),\n\t};\n}\n",
|
|
6
6
|
"import type { S3Client } from 'bun';\nimport type { BucketDefinition } from './types';\n\n/**\n * What every operation of a bound bucket works from, resolved once: the\n * client, the definition, and the accepted content types as a list.\n *\n * It holds **data only**. The operations are plain functions that take it as\n * their first argument, in `guards.ts` and `operations/` — a context of\n * closures would only be the factory this package split up, one size down.\n */\nexport interface BucketContext<P> {\n\treadonly client: S3Client;\n\treadonly definition: BucketDefinition<P>;\n\t/**\n\t * The types the definition accepts, always as a list, **as written** —\n\t * an error message quotes these, and the comparison normalises them.\n\t * `undefined` when the bucket accepts anything.\n\t */\n\treadonly accepted: readonly string[] | undefined;\n}\n\n/** The types a definition accepts, as a list. */\nfunction acceptedTypes(\n\tcontentType: string | readonly string[] | undefined,\n): readonly string[] | undefined {\n\tif (contentType === undefined) return undefined;\n\treturn typeof contentType === 'string' ? [contentType] : contentType;\n}\n\nexport function bucketContext<P>(\n\tclient: S3Client,\n\tdefinition: BucketDefinition<P>,\n): BucketContext<P> {\n\treturn {\n\t\tclient,\n\t\tdefinition,\n\t\taccepted: acceptedTypes(definition.contentType),\n\t};\n}\n\n/** The key this definition builds for these parameters. */\nexport function keyOf<P>(context: BucketContext<P>, params: P): string {\n\treturn context.definition.key(params);\n}\n",
|
|
7
7
|
"import type { S3ListObjectsResponse } from 'bun';\nimport type { BucketContext } from '../context';\nimport type { ObjectPage, StoredObject } from '../types';\n\n/** One page of the bucket, in this repository's cursor shape. */\nexport async function listObjects<P>(\n\tcontext: BucketContext<P>,\n\toptions: { prefix?: string; limit?: number; cursor?: string | null } = {},\n): Promise<ObjectPage> {\n\tconst answer = await context.client.list({\n\t\tprefix: options.prefix,\n\t\tmaxKeys: options.limit,\n\t\tcontinuationToken: options.cursor ?? undefined,\n\t});\n\tconst contents: NonNullable<S3ListObjectsResponse['contents']> =\n\t\tanswer.contents ?? [];\n\tconst items: StoredObject[] = contents.map((found) => ({\n\t\tkey: found.key,\n\t\tsize: found.size,\n\t\tlastModified: found.lastModified ? new Date(found.lastModified) : undefined,\n\t\teTag: found.eTag,\n\t}));\n\t// `isTruncated` is what says there is more, and it is the only thing that\n\t// does: a service that sends a token on the last page anyway would page\n\t// for ever if the token alone decided. `null` is this repository's\n\t// \"no next page\".\n\tconst next = answer.isTruncated\n\t\t? (answer.nextContinuationToken ?? null)\n\t\t: null;\n\treturn { items, nextCursor: next };\n}\n",
|
|
8
|
-
"
|
|
8
|
+
"/** What went wrong. Each one is documented in the README's Traps. */\nexport type S3ErrorCode =\n\t/** The body's content type is not one this bucket accepts. */\n\t| 'WRONG_TYPE'\n\t/** The body is bigger than this bucket's `maxSize`. */\n\t| 'TOO_LARGE'\n\t/** The body's size cannot be known before sending, and `maxSize` is set. */\n\t| 'UNMEASURABLE'\n\t/**\n\t * An option's own value is not one the service accepts: `acl` or\n\t * `storageClass` on a write, `acl` or `expiresIn` on a presigned URL.\n\t */\n\t| 'WRONG_OPTION';\n\n/**\n * This package's only error, and every one of them is thrown **before**\n * anything is sent. S3's own failures come back as they are, from Bun's\n * client.\n */\nexport class S3Error extends Error {\n\treadonly code: S3ErrorCode;\n\t/** The object key it was about — the bucket and the key, never the body. */\n\treadonly key: string;\n\n\tconstructor(code: S3ErrorCode, key: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'S3Error';\n\t\tthis.code = code;\n\t\tthis.key = key;\n\t}\n}\n",
|
|
9
|
+
"import { S3Error } from '../errors/s3-error';\nimport type { BucketContext } from './context';\nimport type { PresignOptions } from './operations/presign';\nimport type { PutBody, PutOptions } from './types';\n\n/**\n * A content type without its parameters, lower-cased: `text/plain` from\n * `text/plain;charset=utf-8`.\n *\n * Both sides of the comparison go through this, because the type a body\n * carries is rarely the bare one a definition names — `Bun.file('a.json')`\n * reports `application/json;charset=utf-8` (measured on bun 1.4.2), and an\n * explicit `IMAGE/PNG` is the same type as `image/png`.\n */\nexport function essenceOf(type: string): string {\n\treturn (type.split(';')[0] ?? '').trim().toLowerCase();\n}\n\n/**\n * The type a write would carry: the one the caller named, or the one the\n * body knows about itself. One function, so the type `check` approves and\n * the type `put` sends can never be two different answers.\n */\nexport function effectiveType(\n\tbody: PutBody,\n\tnamed: string | undefined,\n): string | undefined {\n\t// A Blob carries its own type; anything else has to be told.\n\treturn named ?? (body instanceof Blob ? body.type || undefined : undefined);\n}\n\n/** The body's size, or `undefined` when it cannot be known before sending. */\nexport function sizeOf(body: PutBody): number | undefined {\n\tif (typeof body === 'string') return Buffer.byteLength(body, 'utf8');\n\t// An `S3File` **is** a `Blob` — measured on bun 1.4.2 — and its `size` is\n\t// `NaN`, because nothing has asked the service yet. Returning that would\n\t// pass the guard silently: `NaN > maxSize` is false, whatever `maxSize` is.\n\tif (body instanceof Blob) {\n\t\treturn Number.isFinite(body.size) ? body.size : undefined;\n\t}\n\tif (body instanceof ArrayBuffer) return body.byteLength;\n\tif (ArrayBuffer.isView(body)) return body.byteLength;\n\t// A stream, a `Response`: nothing says how long it is until it has been\n\t// read, which is what `UNMEASURABLE` is about.\n\treturn undefined;\n}\n\n/** Refuses a type the bucket does not accept. Shared by `put` and `presign`. */\nexport function checkType<P>(\n\tcontext: BucketContext<P>,\n\tkey: string,\n\ttype: string | undefined,\n): void {\n\tconst { accepted } = context;\n\tif (!accepted) return;\n\tconst list = accepted.join(', ');\n\tif (!type) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_TYPE',\n\t\t\tkey,\n\t\t\t`\"${context.definition.bucket}\" accepts ${list}, and this write ` +\n\t\t\t\t'names no content type. Pass `type`',\n\t\t);\n\t}\n\tif (!accepted.some((one) => essenceOf(one) === essenceOf(type))) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_TYPE',\n\t\t\tkey,\n\t\t\t`\"${context.definition.bucket}\" accepts ${list}, not ${type}`,\n\t\t);\n\t}\n}\n\n/** Refuses a body the bucket does not accept, before anything is sent. */\nexport function checkSize<P>(\n\tcontext: BucketContext<P>,\n\tkey: string,\n\tbody: PutBody,\n): void {\n\tconst { maxSize, bucket } = context.definition;\n\tif (maxSize === undefined) return;\n\tconst size = sizeOf(body);\n\tif (size === undefined) {\n\t\tthrow new S3Error(\n\t\t\t'UNMEASURABLE',\n\t\t\tkey,\n\t\t\t`\"${bucket}\" has a maxSize, and this body's size cannot be known ` +\n\t\t\t\t'before sending it. Read it into memory first, or drop `maxSize` ' +\n\t\t\t\t'and let the service refuse it',\n\t\t);\n\t}\n\tif (size > maxSize) {\n\t\tthrow new S3Error(\n\t\t\t'TOO_LARGE',\n\t\t\tkey,\n\t\t\t`\"${bucket}\" accepts ${maxSize} bytes at most, and this body is ${size}`,\n\t\t);\n\t}\n}\n\n/**\n * The values the service accepts for the two options that have a fixed set,\n * listed so this package refuses a wrong one itself.\n *\n * Bun checks them too, and refuses with a plain `TypeError` — measured on\n * bun 1.4.2, `name` is `\"TypeError\"`. Every other refusal of the same call\n * is an `S3Error` with a code, so a caller had to catch two classes for one\n * `put`, and read message text for one of them. Checking here gives every\n * refusal of a write one class and one code.\n *\n * The exhaustiveness lines below are the ones `PASSED` carries in `writes.ts`,\n * for the values rather than the keys: a value Bun adds or drops fails the\n * build here instead of silently widening or narrowing what this package\n * accepts.\n */\nconst ACLS = [\n\t'private',\n\t'public-read',\n\t'public-read-write',\n\t'aws-exec-read',\n\t'authenticated-read',\n\t'bucket-owner-read',\n\t'bucket-owner-full-control',\n\t'log-delivery-write',\n] as const satisfies readonly NonNullable<PutOptions['acl']>[];\n\nconst STORAGE_CLASSES = [\n\t'STANDARD',\n\t'DEEP_ARCHIVE',\n\t'EXPRESS_ONEZONE',\n\t'GLACIER',\n\t'GLACIER_IR',\n\t'INTELLIGENT_TIERING',\n\t'ONEZONE_IA',\n\t'OUTPOSTS',\n\t'REDUCED_REDUNDANCY',\n\t'SNOW',\n\t'STANDARD_IA',\n] as const satisfies readonly NonNullable<PutOptions['storageClass']>[];\n\ntype UnlistedAcl = Exclude<\n\tNonNullable<PutOptions['acl']>,\n\t(typeof ACLS)[number]\n>;\nconst _everyAclListed: [UnlistedAcl] extends [never] ? true : UnlistedAcl =\n\ttrue;\nvoid _everyAclListed;\n\ntype UnlistedClass = Exclude<\n\tNonNullable<PutOptions['storageClass']>,\n\t(typeof STORAGE_CLASSES)[number]\n>;\nconst _everyClassListed: [UnlistedClass] extends [never]\n\t? true\n\t: UnlistedClass = true;\nvoid _everyClassListed;\n\nconst ALLOWED = {\n\tacl: ACLS,\n\tstorageClass: STORAGE_CLASSES,\n} as const satisfies Partial<\n\tRecord<keyof PutOptions | keyof PresignOptions, readonly string[]>\n>;\n\n/**\n * The longest a presigned URL can live: SigV4's own limit, seven days.\n *\n * Measured on bun 1.4.2: the client refuses `0` and below with a `TypeError`\n * of its own, and **signs** an `expiresIn` of `1e12` happily — a URL the\n * service then rejects at use time, which is the one thing this package\n * exists not to do.\n */\nconst MAX_EXPIRES_IN = 604_800;\n\n/**\n * Refuses a value the service does not accept for `acl` or `storageClass`.\n * Shared by `put` and `presign`, so the same wrong `acl` is the same error\n * whichever one a caller reached for. An option with no fixed set passes.\n */\nexport function checkOption(key: string, name: string, value: unknown): void {\n\tif (value === undefined) return;\n\tif (name === 'expiresIn') {\n\t\tcheckExpiresIn(key, value);\n\t\treturn;\n\t}\n\tconst allowed = (ALLOWED as Record<string, readonly string[] | undefined>)[\n\t\tname\n\t];\n\tif (!allowed) return;\n\tif (!allowed.includes(value as string)) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_OPTION',\n\t\t\tkey,\n\t\t\t`${name} must be one of ${allowed.join(', ')}; got ${JSON.stringify(value)}`,\n\t\t);\n\t}\n}\n\nfunction checkExpiresIn(key: string, value: unknown): void {\n\tif (\n\t\ttypeof value !== 'number' ||\n\t\t!Number.isFinite(value) ||\n\t\tvalue <= 0 ||\n\t\tvalue > MAX_EXPIRES_IN\n\t) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_OPTION',\n\t\t\tkey,\n\t\t\t`expiresIn is seconds, and must be above 0 and at most ` +\n\t\t\t\t`${MAX_EXPIRES_IN} (seven days, which is S3's own limit); ` +\n\t\t\t\t`got ${JSON.stringify(value)}`,\n\t\t);\n\t}\n}\n",
|
|
10
|
+
"import type { S3Options } from 'bun';\nimport { type BucketContext, keyOf } from '../context';\nimport { checkOption } from '../guards';\n\n/** How a presigned URL is asked for. `expiresIn` is **seconds**, as S3's is. */\nexport interface PresignOptions {\n\t/** Seconds until it expires. Bun's default is a day; give one. */\n\texpiresIn?: number;\n\t/** `public-read` and the rest, when the service honours it. */\n\tacl?: S3Options['acl'];\n}\n\n/**\n * The options this package signs with, and only those.\n *\n * The same rule as a write's, for the same measured reason: Bun's second\n * parameter extends `S3Options`, so a value carrying extra keys at run time —\n * an options bag off a request body, anything that is not a fresh object\n * literal — **redirects the signed URL**. Measured: a `bucket` key signs a\n * URL for another bucket, and a credential key signs it against another\n * endpoint entirely. The types refuse both; a value that never met the types\n * does not.\n */\nconst SIGNED = ['expiresIn', 'acl'] as const satisfies readonly Signable[];\n\ntype Signable = keyof PresignOptions;\ntype Unsigned = Exclude<Signable, (typeof SIGNED)[number]>;\nconst _nothingForgotten: [Unsigned] extends [never] ? true : Unsigned = true;\nvoid _nothingForgotten;\n\nfunction signed(\n\tkey: string,\n\toptions: PresignOptions | undefined,\n): PresignOptions {\n\tconst forwarded: Record<string, unknown> = {};\n\tfor (const name of SIGNED) {\n\t\tconst value = options?.[name];\n\t\tif (value === undefined) continue;\n\t\t// The same allowlist a `put` is held to: a wrong `acl` is an `S3Error`\n\t\t// with `WRONG_OPTION` whichever of the two a caller reached for, rather\n\t\t// than an `S3Error` here and Bun's own `TypeError` there.\n\t\tcheckOption(key, name, value);\n\t\tforwarded[name] = value;\n\t}\n\treturn forwarded as PresignOptions;\n}\n\nexport function presignGetUrl<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\toptions?: PresignOptions,\n): string {\n\tconst key = keyOf(context, params);\n\treturn context.client.presign(key, {\n\t\t...signed(key, options),\n\t\tmethod: 'GET',\n\t});\n}\n\n/**\n * A URL that writes this object, signed.\n *\n * It carries **no content type**, and takes none. Measured on bun 1.4.2:\n * `presign`'s `type` only adds `response-content-type`, S3's override for\n * what a *download* is labelled; `X-Amz-SignedHeaders` stays `host`, so the\n * `Content-Type` the uploader sends is not signed and not constrained. A PUT\n * signed for a `text/csv` bucket stores an `application/zip` body happily —\n * measured against this package's own test service, status 200. Naming a type\n * here would only look like a guarantee.\n */\nexport function presignPutUrl<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\toptions?: PresignOptions,\n): string {\n\tconst key = keyOf(context, params);\n\treturn context.client.presign(key, {\n\t\t...signed(key, options),\n\t\tmethod: 'PUT',\n\t});\n}\n",
|
|
9
11
|
"import type { S3File, S3Stats } from 'bun';\nimport { type BucketContext, keyOf } from '../context';\n\n/**\n * `undefined` rather than a throw when the object is simply not there.\n *\n * It reads straight away and catches the service's own `NoSuchKey`, rather\n * than asking `exists` first: one round trip instead of two, and no window in\n * which an object deleted between the two turns a promised `undefined` into a\n * throw. `stat` is itself a HEAD, so asking first bought nothing at all.\n */\nasync function whenPresent<P, T>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\tread: (file: S3File) => Promise<T>,\n): Promise<T | undefined> {\n\ttry {\n\t\treturn await read(context.client.file(keyOf(context, params)));\n\t} catch (reason) {\n\t\t// Bun names its own S3 failures `S3Error` too, and carries S3's code.\n\t\tif ((reason as { code?: unknown }).code === 'NoSuchKey') return undefined;\n\t\tthrow reason;\n\t}\n}\n\nexport function readBytes<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<Uint8Array | undefined> {\n\treturn whenPresent(context, params, (file) => file.bytes());\n}\n\nexport function readText<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<string | undefined> {\n\treturn whenPresent(context, params, (file) => file.text());\n}\n\nexport function statObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<S3Stats | undefined> {\n\treturn whenPresent(context, params, (file) => file.stat());\n}\n\nexport function objectExists<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<boolean> {\n\treturn context.client.exists(keyOf(context, params));\n}\n",
|
|
10
|
-
"
|
|
11
|
-
"import { S3Error } from '../errors/s3-error';\nimport type { BucketContext } from './context';\nimport type { PutBody } from './types';\n\n/**\n * A content type without its parameters, lower-cased: `text/plain` from\n * `text/plain;charset=utf-8`.\n *\n * Both sides of the comparison go through this, because the type a body\n * carries is rarely the bare one a definition names — `Bun.file('a.json')`\n * reports `application/json;charset=utf-8` (measured on bun 1.4.2), and an\n * explicit `IMAGE/PNG` is the same type as `image/png`.\n */\nexport function essenceOf(type: string): string {\n\treturn (type.split(';')[0] ?? '').trim().toLowerCase();\n}\n\n/**\n * The type a write would carry: the one the caller named, or the one the\n * body knows about itself. One function, so the type `check` approves and\n * the type `put` sends can never be two different answers.\n */\nexport function effectiveType(\n\tbody: PutBody,\n\tnamed: string | undefined,\n): string | undefined {\n\t// A Blob carries its own type; anything else has to be told.\n\treturn named ?? (body instanceof Blob ? body.type || undefined : undefined);\n}\n\n/** The body's size, or `undefined` when it cannot be known before sending. */\nexport function sizeOf(body: PutBody): number | undefined {\n\tif (typeof body === 'string') return Buffer.byteLength(body, 'utf8');\n\t// An `S3File` **is** a `Blob` — measured on bun 1.4.2 — and its `size` is\n\t// `NaN`, because nothing has asked the service yet. Returning that would\n\t// pass the guard silently: `NaN > maxSize` is false, whatever `maxSize` is.\n\tif (body instanceof Blob) {\n\t\treturn Number.isFinite(body.size) ? body.size : undefined;\n\t}\n\tif (body instanceof ArrayBuffer) return body.byteLength;\n\tif (ArrayBuffer.isView(body)) return body.byteLength;\n\t// A stream, a `Response`: nothing says how long it is until it has been\n\t// read, which is what `UNMEASURABLE` is about.\n\treturn undefined;\n}\n\n/** Refuses a type the bucket does not accept. Shared by `put` and `presign`. */\nexport function checkType<P>(\n\tcontext: BucketContext<P>,\n\tkey: string,\n\ttype: string | undefined,\n): void {\n\tconst { accepted } = context;\n\tif (!accepted) return;\n\tconst list = accepted.join(', ');\n\tif (!type) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_TYPE',\n\t\t\tkey,\n\t\t\t`\"${context.definition.bucket}\" accepts ${list}, and this write ` +\n\t\t\t\t'names no content type. Pass `type`',\n\t\t);\n\t}\n\tif (!accepted.some((one) => essenceOf(one) === essenceOf(type))) {\n\t\tthrow new S3Error(\n\t\t\t'WRONG_TYPE',\n\t\t\tkey,\n\t\t\t`\"${context.definition.bucket}\" accepts ${list}, not ${type}`,\n\t\t);\n\t}\n}\n\n/** Refuses a body the bucket does not accept, before anything is sent. */\nexport function checkSize<P>(\n\tcontext: BucketContext<P>,\n\tkey: string,\n\tbody: PutBody,\n): void {\n\tconst { maxSize, bucket } = context.definition;\n\tif (maxSize === undefined) return;\n\tconst size = sizeOf(body);\n\tif (size === undefined) {\n\t\tthrow new S3Error(\n\t\t\t'UNMEASURABLE',\n\t\t\tkey,\n\t\t\t`\"${bucket}\" has a maxSize, and this body's size cannot be known ` +\n\t\t\t\t'before sending it. Read it into memory first, or drop `maxSize` ' +\n\t\t\t\t'and let the service refuse it',\n\t\t);\n\t}\n\tif (size > maxSize) {\n\t\tthrow new S3Error(\n\t\t\t'TOO_LARGE',\n\t\t\tkey,\n\t\t\t`\"${bucket}\" accepts ${maxSize} bytes at most, and this body is ${size}`,\n\t\t);\n\t}\n}\n",
|
|
12
|
-
"import { type BucketContext, keyOf } from '../context';\nimport { checkSize, checkType, effectiveType } from '../guards';\nimport type { PutBody, PutOptions } from '../types';\n\n/**\n * Writes it, once the bucket's content type and size have accepted it. Both\n * guards run before `write` is called, so a refused body is never sent.\n */\nexport async function putObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\tbody: PutBody,\n\toptions: PutOptions = {},\n): Promise<void> {\n\tconst key = keyOf(context, params);\n\tconst type = effectiveType(body, options.type);\n\tcheckType(context, key, type);\n\tcheckSize(context, key, body);\n\t// The very type `checkType` approved, and nothing else: `type` is not one\n\t// of the keys `passed` forwards, so no option can carry a second one in\n\t// beside it.\n\tawait context.client.write(key, body, {\n\t\t...passed(options),\n\t\t...(type ? { type } : {}),\n\t});\n}\n\n/**\n * The options this package forwards, and only those.\n *\n * `PutOptions` refuses the rest at compile time, and that is not enough:\n * measured, spreading the caller's object straight through let a `bucket`\n * key **redirect the write to another bucket** — the object was stored\n * somewhere the definition never described, and the call reported success.\n * Options that arrive from outside a handler are not typed, so the list is\n * applied at run time as well. A key that is not here is dropped, never sent.\n */\ntype Forwardable = Exclude<keyof PutOptions, 'type'>;\n\nconst PASSED = [\n\t'acl',\n\t'storageClass',\n\t'contentDisposition',\n\t'contentEncoding',\n] as const satisfies readonly Forwardable[];\n\n/**\n * `satisfies` proves every key listed is real; it proves nothing about one\n * that is **missing**. Widen `PutOptions` and forget to list the new key\n * here, and the type would advertise an option the run time silently drops —\n * which is the failure this allowlist exists to prevent, in the other\n * direction. This line is what fails the build instead.\n */\ntype Unforwarded = Exclude<Forwardable, (typeof PASSED)[number]>;\nconst _nothingForgotten: [Unforwarded] extends [never] ? true : Unforwarded =\n\ttrue;\nvoid _nothingForgotten;\n\nfunction passed(options: PutOptions): PutOptions {\n\tconst forwarded: Record<string, unknown> = {};\n\tfor (const key of PASSED) {\n\t\tif (options[key] !== undefined) forwarded[key] = options[key];\n\t}\n\treturn forwarded as PutOptions;\n}\n\n/** Removes it. S3 does not say whether anything was there, and nor does this. */\nexport function deleteObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<void> {\n\treturn context.client.delete(keyOf(context, params));\n}\n",
|
|
12
|
+
"import { type BucketContext, keyOf } from '../context';\nimport { checkOption, checkSize, checkType, effectiveType } from '../guards';\nimport type { PutBody, PutOptions } from '../types';\n\n/**\n * Writes it, once the bucket's content type and size have accepted it. Both\n * guards run before `write` is called, so a refused body is never sent.\n */\nexport async function putObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\tbody: PutBody,\n\toptions: PutOptions = {},\n): Promise<void> {\n\tconst key = keyOf(context, params);\n\tconst type = effectiveType(body, options.type);\n\tcheckType(context, key, type);\n\tcheckSize(context, key, body);\n\t// Refused here with the other two, so every guard on this call answers\n\t// before anything is sent, and every one of them is an `S3Error`.\n\tconst forwarded = passed(key, options);\n\t// The very type `checkType` approved, and nothing else: `type` is not one\n\t// of the keys `passed` forwards, so no option can carry a second one in\n\t// beside it.\n\tawait context.client.write(key, body, {\n\t\t...forwarded,\n\t\t...(type ? { type } : {}),\n\t});\n}\n\n/**\n * The options this package forwards, and only those.\n *\n * `PutOptions` refuses the rest at compile time, and that is not enough:\n * measured, spreading the caller's object straight through let a `bucket`\n * key **redirect the write to another bucket** — the object was stored\n * somewhere the definition never described, and the call reported success.\n * Options that arrive from outside a handler are not typed, so the list is\n * applied at run time as well. A key that is not here is dropped, never sent.\n */\ntype Forwardable = Exclude<keyof PutOptions, 'type'>;\n\nconst PASSED = [\n\t'acl',\n\t'storageClass',\n\t'contentDisposition',\n\t'contentEncoding',\n] as const satisfies readonly Forwardable[];\n\n/**\n * `satisfies` proves every key listed is real; it proves nothing about one\n * that is **missing**. Widen `PutOptions` and forget to list the new key\n * here, and the type would advertise an option the run time silently drops —\n * which is the failure this allowlist exists to prevent, in the other\n * direction. This line is what fails the build instead.\n */\ntype Unforwarded = Exclude<Forwardable, (typeof PASSED)[number]>;\nconst _nothingForgotten: [Unforwarded] extends [never] ? true : Unforwarded =\n\ttrue;\nvoid _nothingForgotten;\n\nfunction passed(key: string, options: PutOptions): PutOptions {\n\tconst forwarded: Record<string, unknown> = {};\n\tfor (const name of PASSED) {\n\t\tconst value = options[name];\n\t\tif (value === undefined) continue;\n\t\tcheckOption(key, name, value);\n\t\tforwarded[name] = value;\n\t}\n\treturn forwarded as PutOptions;\n}\n\n/** Removes it. S3 does not say whether anything was there, and nor does this. */\nexport function deleteObject<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n): Promise<void> {\n\treturn context.client.delete(keyOf(context, params));\n}\n",
|
|
13
13
|
"import type { BucketDefinition } from './types';\n\n/**\n * Describes a bucket. It talks to nothing: `bindBucket` is what needs\n * credentials.\n *\n * ```ts\n * export const avatars = defineBucket({\n * \tbucket: 'avatars',\n * \tkey: (p: { userId: string }) => `${p.userId}.png`,\n * \tcontentType: ['image/png', 'image/jpeg'],\n * \tmaxSize: 2 * 1024 * 1024,\n * });\n * ```\n */\nexport function defineBucket<P>(\n\tdefinition: BucketDefinition<P>,\n): BucketDefinition<P> {\n\tif (definition.bucket.length === 0) {\n\t\tthrow new TypeError('defineBucket: a bucket definition needs a bucket');\n\t}\n\tif (definition.maxSize !== undefined) {\n\t\tconst { maxSize } = definition;\n\t\tif (!Number.isFinite(maxSize) || maxSize <= 0) {\n\t\t\tthrow new TypeError(\n\t\t\t\t`defineBucket: \"${definition.bucket}\" has a maxSize of ${maxSize}; ` +\n\t\t\t\t\t'it is a number of bytes, and must be above zero',\n\t\t\t);\n\t\t}\n\t}\n\tconst types = definition.contentType;\n\tif (Array.isArray(types) && types.length === 0) {\n\t\tthrow new TypeError(\n\t\t\t`defineBucket: \"${definition.bucket}\" accepts an empty list of ` +\n\t\t\t\t'content types, so nothing could ever be written. Leave ' +\n\t\t\t\t'`contentType` out to accept anything',\n\t\t);\n\t}\n\treturn Object.freeze({ ...definition });\n}\n"
|
|
14
14
|
],
|
|
15
|
-
"mappings": ";AAAA;;;ACuBA,SAAS,aAAa,CACrB,aACgC;AAAA,EAChC,IAAI,gBAAgB;AAAA,IAAW;AAAA,EAC/B,OAAO,OAAO,gBAAgB,WAAW,CAAC,WAAW,IAAI;AAAA;AAGnD,SAAS,aAAgB,CAC/B,QACA,YACmB;AAAA,EACnB,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA,UAAU,cAAc,WAAW,WAAW;AAAA,EAC/C;AAAA;AAIM,SAAS,KAAQ,CAAC,SAA2B,QAAmB;AAAA,EACtE,OAAO,QAAQ,WAAW,IAAI,MAAM;AAAA;;;ACtCrC,eAAsB,WAAc,CACnC,SACA,UAAuE,CAAC,GAClD;AAAA,EACtB,MAAM,SAAS,MAAM,QAAQ,OAAO,KAAK;AAAA,IACxC,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,mBAAmB,QAAQ,UAAU;AAAA,EACtC,CAAC;AAAA,EACD,MAAM,WACL,OAAO,YAAY,CAAC;AAAA,EACrB,MAAM,QAAwB,SAAS,IAAI,CAAC,WAAW;AAAA,IACtD,KAAK,MAAM;AAAA,IACX,MAAM,MAAM;AAAA,IACZ,cAAc,MAAM,eAAe,IAAI,KAAK,MAAM,YAAY,IAAI;AAAA,IAClE,MAAM,MAAM;AAAA,EACb,EAAE;AAAA,EAKF,MAAM,OAAO,OAAO,cAChB,OAAO,yBAAyB,OACjC;AAAA,EACH,OAAO,EAAE,OAAO,YAAY,KAAK;AAAA;;;
|
|
16
|
-
"debugId": "
|
|
15
|
+
"mappings": ";AAAA;;;ACuBA,SAAS,aAAa,CACrB,aACgC;AAAA,EAChC,IAAI,gBAAgB;AAAA,IAAW;AAAA,EAC/B,OAAO,OAAO,gBAAgB,WAAW,CAAC,WAAW,IAAI;AAAA;AAGnD,SAAS,aAAgB,CAC/B,QACA,YACmB;AAAA,EACnB,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA,UAAU,cAAc,WAAW,WAAW;AAAA,EAC/C;AAAA;AAIM,SAAS,KAAQ,CAAC,SAA2B,QAAmB;AAAA,EACtE,OAAO,QAAQ,WAAW,IAAI,MAAM;AAAA;;;ACtCrC,eAAsB,WAAc,CACnC,SACA,UAAuE,CAAC,GAClD;AAAA,EACtB,MAAM,SAAS,MAAM,QAAQ,OAAO,KAAK;AAAA,IACxC,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,mBAAmB,QAAQ,UAAU;AAAA,EACtC,CAAC;AAAA,EACD,MAAM,WACL,OAAO,YAAY,CAAC;AAAA,EACrB,MAAM,QAAwB,SAAS,IAAI,CAAC,WAAW;AAAA,IACtD,KAAK,MAAM;AAAA,IACX,MAAM,MAAM;AAAA,IACZ,cAAc,MAAM,eAAe,IAAI,KAAK,MAAM,YAAY,IAAI;AAAA,IAClE,MAAM,MAAM;AAAA,EACb,EAAE;AAAA,EAKF,MAAM,OAAO,OAAO,cAChB,OAAO,yBAAyB,OACjC;AAAA,EACH,OAAO,EAAE,OAAO,YAAY,KAAK;AAAA;;;ACV3B,MAAM,gBAAgB,MAAM;AAAA,EAKlC,WAAW,CAAC,MAAmB,KAAa,SAAiB;AAAA,IAC5D,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA,IACZ,KAAK,OAAO;AAAA,IACZ,KAAK,MAAM;AAAA;AAEb;;;AChBO,SAAS,SAAS,CAAC,MAAsB;AAAA,EAC/C,QAAQ,KAAK,MAAM,GAAG,EAAE,MAAM,IAAI,KAAK,EAAE,YAAY;AAAA;AAQ/C,SAAS,aAAa,CAC5B,MACA,OACqB;AAAA,EAErB,OAAO,UAAU,gBAAgB,OAAO,KAAK,QAAQ,YAAY;AAAA;AAI3D,SAAS,MAAM,CAAC,MAAmC;AAAA,EACzD,IAAI,OAAO,SAAS;AAAA,IAAU,OAAO,OAAO,WAAW,MAAM,MAAM;AAAA,EAInE,IAAI,gBAAgB,MAAM;AAAA,IACzB,OAAO,OAAO,SAAS,KAAK,IAAI,IAAI,KAAK,OAAO;AAAA,EACjD;AAAA,EACA,IAAI,gBAAgB;AAAA,IAAa,OAAO,KAAK;AAAA,EAC7C,IAAI,YAAY,OAAO,IAAI;AAAA,IAAG,OAAO,KAAK;AAAA,EAG1C;AAAA;AAIM,SAAS,SAAY,CAC3B,SACA,KACA,MACO;AAAA,EACP,QAAQ,aAAa;AAAA,EACrB,IAAI,CAAC;AAAA,IAAU;AAAA,EACf,MAAM,OAAO,SAAS,KAAK,IAAI;AAAA,EAC/B,IAAI,CAAC,MAAM;AAAA,IACV,MAAM,IAAI,QACT,cACA,KACA,IAAI,QAAQ,WAAW,mBAAmB,0BACzC,oCACF;AAAA,EACD;AAAA,EACA,IAAI,CAAC,SAAS,KAAK,CAAC,QAAQ,UAAU,GAAG,MAAM,UAAU,IAAI,CAAC,GAAG;AAAA,IAChE,MAAM,IAAI,QACT,cACA,KACA,IAAI,QAAQ,WAAW,mBAAmB,aAAa,MACxD;AAAA,EACD;AAAA;AAIM,SAAS,SAAY,CAC3B,SACA,KACA,MACO;AAAA,EACP,QAAQ,SAAS,WAAW,QAAQ;AAAA,EACpC,IAAI,YAAY;AAAA,IAAW;AAAA,EAC3B,MAAM,OAAO,OAAO,IAAI;AAAA,EACxB,IAAI,SAAS,WAAW;AAAA,IACvB,MAAM,IAAI,QACT,gBACA,KACA,IAAI,iEACH,qEACA,+BACF;AAAA,EACD;AAAA,EACA,IAAI,OAAO,SAAS;AAAA,IACnB,MAAM,IAAI,QACT,aACA,KACA,IAAI,mBAAmB,2CAA2C,MACnE;AAAA,EACD;AAAA;AAkBD,IAAM,OAAO;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAEA,IAAM,kBAAkB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAmBA,IAAM,UAAU;AAAA,EACf,KAAK;AAAA,EACL,cAAc;AACf;AAYA,IAAM,iBAAiB;AAOhB,SAAS,WAAW,CAAC,KAAa,MAAc,OAAsB;AAAA,EAC5E,IAAI,UAAU;AAAA,IAAW;AAAA,EACzB,IAAI,SAAS,aAAa;AAAA,IACzB,eAAe,KAAK,KAAK;AAAA,IACzB;AAAA,EACD;AAAA,EACA,MAAM,UAAW,QAChB;AAAA,EAED,IAAI,CAAC;AAAA,IAAS;AAAA,EACd,IAAI,CAAC,QAAQ,SAAS,KAAe,GAAG;AAAA,IACvC,MAAM,IAAI,QACT,gBACA,KACA,GAAG,uBAAuB,QAAQ,KAAK,IAAI,UAAU,KAAK,UAAU,KAAK,GAC1E;AAAA,EACD;AAAA;AAGD,SAAS,cAAc,CAAC,KAAa,OAAsB;AAAA,EAC1D,IACC,OAAO,UAAU,YACjB,CAAC,OAAO,SAAS,KAAK,KACtB,SAAS,KACT,QAAQ,gBACP;AAAA,IACD,MAAM,IAAI,QACT,gBACA,KACA,2DACC,GAAG,2DACH,OAAO,KAAK,UAAU,KAAK,GAC7B;AAAA,EACD;AAAA;;;AC7LD,IAAM,SAAS,CAAC,aAAa,KAAK;AAOlC,SAAS,MAAM,CACd,KACA,SACiB;AAAA,EACjB,MAAM,YAAqC,CAAC;AAAA,EAC5C,WAAW,QAAQ,QAAQ;AAAA,IAC1B,MAAM,QAAQ,UAAU;AAAA,IACxB,IAAI,UAAU;AAAA,MAAW;AAAA,IAIzB,YAAY,KAAK,MAAM,KAAK;AAAA,IAC5B,UAAU,QAAQ;AAAA,EACnB;AAAA,EACA,OAAO;AAAA;AAGD,SAAS,aAAgB,CAC/B,SACA,QACA,SACS;AAAA,EACT,MAAM,MAAM,MAAM,SAAS,MAAM;AAAA,EACjC,OAAO,QAAQ,OAAO,QAAQ,KAAK;AAAA,OAC/B,OAAO,KAAK,OAAO;AAAA,IACtB,QAAQ;AAAA,EACT,CAAC;AAAA;AAcK,SAAS,aAAgB,CAC/B,SACA,QACA,SACS;AAAA,EACT,MAAM,MAAM,MAAM,SAAS,MAAM;AAAA,EACjC,OAAO,QAAQ,OAAO,QAAQ,KAAK;AAAA,OAC/B,OAAO,KAAK,OAAO;AAAA,IACtB,QAAQ;AAAA,EACT,CAAC;AAAA;;;ACpEF,eAAe,WAAiB,CAC/B,SACA,QACA,MACyB;AAAA,EACzB,IAAI;AAAA,IACH,OAAO,MAAM,KAAK,QAAQ,OAAO,KAAK,MAAM,SAAS,MAAM,CAAC,CAAC;AAAA,IAC5D,OAAO,QAAQ;AAAA,IAEhB,IAAK,OAA8B,SAAS;AAAA,MAAa;AAAA,IACzD,MAAM;AAAA;AAAA;AAID,SAAS,SAAY,CAC3B,SACA,QACkC;AAAA,EAClC,OAAO,YAAY,SAAS,QAAQ,CAAC,SAAS,KAAK,MAAM,CAAC;AAAA;AAGpD,SAAS,QAAW,CAC1B,SACA,QAC8B;AAAA,EAC9B,OAAO,YAAY,SAAS,QAAQ,CAAC,SAAS,KAAK,KAAK,CAAC;AAAA;AAGnD,SAAS,UAAa,CAC5B,SACA,QAC+B;AAAA,EAC/B,OAAO,YAAY,SAAS,QAAQ,CAAC,SAAS,KAAK,KAAK,CAAC;AAAA;AAGnD,SAAS,YAAe,CAC9B,SACA,QACmB;AAAA,EACnB,OAAO,QAAQ,OAAO,OAAO,MAAM,SAAS,MAAM,CAAC;AAAA;;;AC1CpD,eAAsB,SAAY,CACjC,SACA,QACA,MACA,UAAsB,CAAC,GACP;AAAA,EAChB,MAAM,MAAM,MAAM,SAAS,MAAM;AAAA,EACjC,MAAM,OAAO,cAAc,MAAM,QAAQ,IAAI;AAAA,EAC7C,UAAU,SAAS,KAAK,IAAI;AAAA,EAC5B,UAAU,SAAS,KAAK,IAAI;AAAA,EAG5B,MAAM,YAAY,OAAO,KAAK,OAAO;AAAA,EAIrC,MAAM,QAAQ,OAAO,MAAM,KAAK,MAAM;AAAA,OAClC;AAAA,OACC,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,EACxB,CAAC;AAAA;AAeF,IAAM,SAAS;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAcA,SAAS,MAAM,CAAC,KAAa,SAAiC;AAAA,EAC7D,MAAM,YAAqC,CAAC;AAAA,EAC5C,WAAW,QAAQ,QAAQ;AAAA,IAC1B,MAAM,QAAQ,QAAQ;AAAA,IACtB,IAAI,UAAU;AAAA,MAAW;AAAA,IACzB,YAAY,KAAK,MAAM,KAAK;AAAA,IAC5B,UAAU,QAAQ;AAAA,EACnB;AAAA,EACA,OAAO;AAAA;AAID,SAAS,YAAe,CAC9B,SACA,QACgB;AAAA,EAChB,OAAO,QAAQ,OAAO,OAAO,MAAM,SAAS,MAAM,CAAC;AAAA;;;APK7C,SAAS,UAAa,CAC5B,YACA,UAAqC,CAAC,GACrB;AAAA,EACjB,MAAM,SAAS,IAAI,SAAS,KAAK,SAAS,QAAQ,WAAW,OAAO,CAAC;AAAA,EACrE,MAAM,UAAU,cAAc,QAAQ,UAAU;AAAA,EAChD,OAAO;AAAA,IACN;AAAA,IACA,QAAQ,CAAC,WAAW,MAAM,SAAS,MAAM;AAAA,IACzC,MAAM,CAAC,WAAW,OAAO,KAAK,MAAM,SAAS,MAAM,CAAC;AAAA,IACpD,KAAK,CAAC,QAAQ,MAAM,eACnB,UAAU,SAAS,QAAQ,MAAM,UAAU;AAAA,IAC5C,OAAO,CAAC,WAAW,UAAU,SAAS,MAAM;AAAA,IAC5C,MAAM,CAAC,WAAW,SAAS,SAAS,MAAM;AAAA,IAC1C,QAAQ,CAAC,WAAW,aAAa,SAAS,MAAM;AAAA,IAChD,MAAM,CAAC,WAAW,WAAW,SAAS,MAAM;AAAA,IAC5C,QAAQ,CAAC,WAAW,aAAa,SAAS,MAAM;AAAA,IAChD,MAAM,CAAC,gBAAgB,YAAY,SAAS,WAAW;AAAA,IACvD,YAAY,CAAC,QAAQ,mBACpB,cAAc,SAAS,QAAQ,cAAc;AAAA,IAC9C,YAAY,CAAC,QAAQ,mBACpB,cAAc,SAAS,QAAQ,cAAc;AAAA,EAC/C;AAAA;;AQzFM,SAAS,YAAe,CAC9B,YACsB;AAAA,EACtB,IAAI,WAAW,OAAO,WAAW,GAAG;AAAA,IACnC,MAAM,IAAI,UAAU,kDAAkD;AAAA,EACvE;AAAA,EACA,IAAI,WAAW,YAAY,WAAW;AAAA,IACrC,QAAQ,YAAY;AAAA,IACpB,IAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAAA,MAC9C,MAAM,IAAI,UACT,kBAAkB,WAAW,4BAA4B,cACxD,iDACF;AAAA,IACD;AAAA,EACD;AAAA,EACA,MAAM,QAAQ,WAAW;AAAA,EACzB,IAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAAG;AAAA,IAC/C,MAAM,IAAI,UACT,kBAAkB,WAAW,sCAC5B,4DACA,sCACF;AAAA,EACD;AAAA,EACA,OAAO,OAAO,OAAO,KAAK,WAAW,CAAC;AAAA;",
|
|
16
|
+
"debugId": "B73383C85513ABE564756E2164756E21",
|
|
17
17
|
"names": []
|
|
18
18
|
}
|
|
@@ -43,9 +43,36 @@ interface PresignOptions {
|
|
|
43
43
|
|
|
44
44
|
| Option | Type | Default | Effect |
|
|
45
45
|
| --- | --- | --- | --- |
|
|
46
|
-
| `expiresIn` | `number` | Bun's, one day | **seconds** until the URL expires. Always pass one |
|
|
46
|
+
| `expiresIn` | `number` | Bun's, one day | **seconds** until the URL expires. Always pass one. Above 604 800 (seven days, S3's own limit), at or below zero, or anything that is not a finite number is an `S3Error` with `code: 'WRONG_OPTION'` |
|
|
47
47
|
| `acl` | `'private' \| 'public-read' \| …` | none | the ACL the URL is signed for, where the service honours it |
|
|
48
48
|
|
|
49
|
+
`acl` goes through the same allowlist a `put` is held to, so a value the
|
|
50
|
+
service does not accept is an `S3Error` with `code: 'WRONG_OPTION'` and no
|
|
51
|
+
URL is signed:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { S3Error } from '@nxgt/s3';
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
store.presignPut({ userId: 'u1' }, { acl: 'everyone' as never });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof S3Error && error.code === 'WRONG_OPTION') {
|
|
60
|
+
error.key; // 'u1.png' — the object it was about
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`expiresIn` goes through the same check. Measured on bun 1.4.2, the client
|
|
66
|
+
refuses `0` and below itself, and **signs** an `expiresIn` of `1e12`
|
|
67
|
+
happily — a URL S3 then rejects at use time, which is the one thing this
|
|
68
|
+
package exists not to do. Both ends are refused here now, before anything is
|
|
69
|
+
signed.
|
|
70
|
+
|
|
71
|
+
Before 0.3.0 the values went straight to the client: the same mistake was two
|
|
72
|
+
different classes depending on whether it reached a `put` or a `presign`.
|
|
73
|
+
There is no `storageClass` here — signing a URL stores nothing, so there is
|
|
74
|
+
no class to name.
|
|
75
|
+
|
|
49
76
|
A day is a long time for a URL that anyone can forward. Sign for the time the
|
|
50
77
|
page actually needs:
|
|
51
78
|
|
package/docs/guide/writes.md
CHANGED
|
@@ -89,27 +89,39 @@ that changes one is a compile error here rather than a silent drift. Given
|
|
|
89
89
|
both here and to `bindBucket`, the one on the write wins: it is the last
|
|
90
90
|
thing handed to the client.
|
|
91
91
|
|
|
92
|
-
### An option's *value* is
|
|
92
|
+
### An option's *value* is checked here too
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
`acl` and `storageClass` are unions, and the values listed above are the only
|
|
95
|
+
ones the service takes. A bag off a request body never met those types, so
|
|
96
|
+
the value is checked at run time as well — before anything is sent, with the
|
|
97
|
+
content type and the size, and as the same `S3Error`:
|
|
97
98
|
|
|
98
99
|
```ts
|
|
99
|
-
import type
|
|
100
|
+
import { S3Error, type PutOptions } from '@nxgt/s3';
|
|
100
101
|
|
|
101
|
-
const bad = { type: 'text/plain', storageClass: '
|
|
102
|
+
const bad = { type: 'text/plain', storageClass: 'CHEAP' } as unknown as PutOptions;
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const error = (await store
|
|
105
|
+
.put({ userId: 'u1' }, 'a,b\n', bad)
|
|
106
|
+
.catch((reason: unknown) => reason)) as S3Error;
|
|
107
|
+
|
|
108
|
+
error instanceof S3Error; // true
|
|
109
|
+
error.code; // 'WRONG_OPTION'
|
|
110
|
+
error.message; // 'storageClass must be one of STANDARD, DEEP_ARCHIVE, …; got "CHEAP"'
|
|
111
|
+
error.key; // 'u1.png' — the key, never the body
|
|
112
|
+
// Nothing was stored: `await store.exists({ userId: 'u1' })` is still false.
|
|
106
113
|
```
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
Bun checks both values as well, and refuses with a plain `TypeError`
|
|
116
|
+
(measured on bun 1.4.2). That left one `put` with two classes of refusal —
|
|
117
|
+
an `S3Error` with a code for the content type and the size, a `TypeError`
|
|
118
|
+
with only a sentence for these two. Checking here means one `put` has one
|
|
119
|
+
class of refusal, with a code to switch on. `contentDisposition` and `contentEncoding` are free strings
|
|
120
|
+
and are never refused, by this package or by Bun.
|
|
121
|
+
|
|
122
|
+
The same allowlist holds `acl` on a
|
|
123
|
+
[presigned URL](presigned-urls.md#options), so a wrong value is the same
|
|
124
|
+
`WRONG_OPTION` whichever call a consumer reached for.
|
|
113
125
|
|
|
114
126
|
### A write cannot change where it goes
|
|
115
127
|
|
|
@@ -165,6 +177,7 @@ try {
|
|
|
165
177
|
| `WRONG_TYPE` | the type is not one this bucket accepts — or the write named none and the body carries none, while the bucket names some |
|
|
166
178
|
| `TOO_LARGE` | the body is bigger than `maxSize`. The limit is inclusive: with `maxSize: 1024`, 1024 bytes passes and 1025 does not |
|
|
167
179
|
| `UNMEASURABLE` | `maxSize` is set and the body's size cannot be known before sending |
|
|
180
|
+
| `WRONG_OPTION` | the write's `acl` or `storageClass` is not a value the service accepts — see [above](#an-options-value-is-checked-here-too) |
|
|
168
181
|
|
|
169
182
|
A content type is compared on its **essence**: parameters and case are
|
|
170
183
|
ignored, and nothing else is. A bucket that accepts `text/csv` accepts
|
package/docs/roadmap.md
CHANGED
|
@@ -39,6 +39,12 @@ _Nothing queued._
|
|
|
39
39
|
|
|
40
40
|
## Shipped
|
|
41
41
|
|
|
42
|
+
- **An option's own value is refused before anything is sent or signed** —
|
|
43
|
+
`code: 'WRONG_OPTION'` for an `acl` or a `storageClass` a write names, for
|
|
44
|
+
an `acl` on a presigned URL, and for an `expiresIn` outside the seven days
|
|
45
|
+
S3 itself allows. Every refusal of a write or a signed URL is one class with
|
|
46
|
+
a code to switch on, rather than an `S3Error` for the content type and the
|
|
47
|
+
size and the client's own `TypeError` for the rest — 0.3.0.
|
|
42
48
|
- **Documentation that travels with the package** — a guide page for the
|
|
43
49
|
bucket definition, writes, reads and presigned URLs, a troubleshooting page
|
|
44
50
|
whose headings are the exact error text, and this roadmap, installed in
|
package/docs/troubleshooting.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Troubleshooting
|
|
2
2
|
|
|
3
3
|
This package throws one error of its own, `S3Error`, with a `code` of
|
|
4
|
-
`WRONG_TYPE`, `TOO_LARGE` or `
|
|
5
|
-
about — never the body. Every one of them is raised **before**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
`WRONG_TYPE`, `TOO_LARGE`, `UNMEASURABLE` or `WRONG_OPTION`, and the object
|
|
5
|
+
`key` it was about — never the body. Every one of them is raised **before**
|
|
6
|
+
anything is sent, so a refused write stored nothing. The service's own
|
|
7
|
+
failures come back as Bun raises them; the entries below say which is which.
|
|
8
|
+
A wrong `acl` is this package's own refusal on a `put` **and** on a
|
|
9
|
+
`presign`, so one class and one code cover both. Bun names its own *service*
|
|
10
|
+
failures `S3Error` as well, so it is `instanceof S3Error` against the class
|
|
11
|
+
this package exports that tells those apart — never `error.name`. (Bun's
|
|
12
|
+
refusal of a wrong argument is a different thing again: a plain `TypeError`,
|
|
13
|
+
named `"TypeError"`.) The Bun messages were measured on Bun 1.4.2.
|
|
9
14
|
|
|
10
15
|
- **Install and import**
|
|
11
16
|
- [`Cannot find package 'bun'`](#cannot-find-package-bun)
|
|
@@ -19,8 +24,9 @@ The Bun messages were measured on Bun 1.4.2.
|
|
|
19
24
|
- [``"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
25
|
- [`"avatars" accepts 2097152 bytes at most, and this body is 5242880`](#avatars-accepts-2097152-bytes-at-most-and-this-body-is-5242880)
|
|
21
26
|
- [`"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
|
|
23
|
-
- [`acl must be one of
|
|
27
|
+
- [`storageClass must be one of STANDARD, DEEP_ARCHIVE, EXPRESS_ONEZONE, …; got "CHEAP"`](#storageclass-must-be-one-of-standard-deep_archive-express_onezone--got-cheap)
|
|
28
|
+
- [`acl must be one of private, public-read, public-read-write, …; got "everyone"`](#acl-must-be-one-of-private-public-read-public-read-write--got-everyone)
|
|
29
|
+
- [`expiresIn is seconds, and must be above 0 and at most 604800 …`](#expiresin-is-seconds-and-must-be-above-0-and-at-most-604800-seven-days-which-is-s3s-own-limit-got-1000000000000)
|
|
24
30
|
- **The service**
|
|
25
31
|
- [`Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required`](#missing-s3-credentials-accesskeyid-secretaccesskey-bucket-and-endpoint-are-required)
|
|
26
32
|
- [`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)
|
|
@@ -96,7 +102,9 @@ defineBucket({ bucket: 'avatars', key, contentType: ['image/png', 'image/jpeg']
|
|
|
96
102
|
|
|
97
103
|
### `"avatars" accepts image/png, image/jpeg, not application/pdf`
|
|
98
104
|
|
|
99
|
-
**When:** `put
|
|
105
|
+
**When:** `put`, with a `type` the definition does not list. A presigned URL
|
|
106
|
+
is never checked against it — `PresignOptions` has no `type`, and a signed
|
|
107
|
+
PUT constrains the key and the deadline and nothing else.
|
|
100
108
|
**Why:** an `S3Error` with `code: 'WRONG_TYPE'`. Nothing was sent. The type
|
|
101
109
|
is compared on its **essence**: `text/csv` accepts `text/csv;charset=utf-8`
|
|
102
110
|
and `TEXT/CSV`, because that is what real bodies carry — parameters and case
|
|
@@ -157,19 +165,31 @@ await avatars.put({ userId }, await response.bytes()); // read it in first
|
|
|
157
165
|
Or drop `maxSize` from the definition and let the service refuse an oversized
|
|
158
166
|
body.
|
|
159
167
|
|
|
160
|
-
### `storageClass must be one of
|
|
168
|
+
### `storageClass must be one of STANDARD, DEEP_ARCHIVE, EXPRESS_ONEZONE, …; got "CHEAP"`
|
|
161
169
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
The whole line names every class the service takes, then the one it was
|
|
171
|
+
given:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
storageClass must be one of STANDARD, DEEP_ARCHIVE, EXPRESS_ONEZONE, GLACIER,
|
|
175
|
+
GLACIER_IR, INTELLIGENT_TIERING, ONEZONE_IA, OUTPOSTS, REDUCED_REDUNDANCY,
|
|
176
|
+
SNOW, STANDARD_IA; got "CHEAP"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**When:** `put` with a `storageClass` that is not one of those — usually a
|
|
180
|
+
value read off a request body or an environment variable, where the types
|
|
181
|
+
were not there to refuse it.
|
|
182
|
+
**Why:** an `S3Error` with `code: 'WRONG_OPTION'`, raised before the request
|
|
183
|
+
goes out, like the content type and the size. Since 0.3.0 this package
|
|
184
|
+
checks the two guarded options itself rather than letting the client refuse
|
|
185
|
+
them, so one `catch` covers every refusal of a `put`.
|
|
186
|
+
**Fix:** narrow the value against the option's own type before it reaches the
|
|
187
|
+
call:
|
|
168
188
|
|
|
169
189
|
```ts
|
|
170
|
-
import type
|
|
190
|
+
import { S3Error, type PutOptions } from '@nxgt/s3';
|
|
171
191
|
|
|
172
|
-
// the classes this application allows, proved against
|
|
192
|
+
// the classes this application allows, proved against the option's type
|
|
173
193
|
const CLASSES = ['STANDARD', 'STANDARD_IA'] as const satisfies readonly NonNullable<
|
|
174
194
|
PutOptions['storageClass']
|
|
175
195
|
>[];
|
|
@@ -178,24 +198,56 @@ function storageClassOf(value: string | undefined): PutOptions['storageClass'] {
|
|
|
178
198
|
return CLASSES.find((known) => known === value); // undefined: the bucket's default
|
|
179
199
|
}
|
|
180
200
|
|
|
181
|
-
|
|
201
|
+
try {
|
|
202
|
+
await avatars.put({ userId }, bytes, { storageClass: storageClassOf(input) });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof S3Error && error.code === 'WRONG_OPTION') return badRequest();
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
182
207
|
```
|
|
183
208
|
|
|
184
|
-
|
|
209
|
+
### `acl must be one of private, public-read, public-read-write, …; got "everyone"`
|
|
185
210
|
|
|
186
|
-
|
|
211
|
+
```text
|
|
212
|
+
acl must be one of private, public-read, public-read-write, aws-exec-read,
|
|
213
|
+
authenticated-read, bucket-owner-read, bucket-owner-full-control,
|
|
214
|
+
log-delivery-write; got "everyone"
|
|
215
|
+
```
|
|
187
216
|
|
|
188
|
-
**When:** `put`, `presignGet` or `presignPut` with an `acl`
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
**When:** `put`, `presignGet` or `presignPut` with an `acl` that is not one
|
|
218
|
+
of those. Both paths go through the same allowlist since 0.3.0; before it,
|
|
219
|
+
`presign` handed the value to the client, which refused it with a plain
|
|
220
|
+
`TypeError` that quoted **every accepted value** (`must be one of "private",
|
|
221
|
+
"public-read", …`). This one quotes only the value it was given, so a message
|
|
222
|
+
with the allowed values in quotes means the package is older than 0.3.0.
|
|
223
|
+
**Why:** the same `code: 'WRONG_OPTION'`, before anything is sent or signed.
|
|
224
|
+
The two guarded options are `acl` and `storageClass`; `contentDisposition`
|
|
225
|
+
and `contentEncoding` are plain strings to the service and accept anything.
|
|
226
|
+
`presign` takes no `storageClass` — nothing is stored by signing a URL.
|
|
193
227
|
**Fix:**
|
|
194
228
|
|
|
195
229
|
```ts
|
|
196
230
|
await avatars.put({ userId }, bytes, { acl: 'public-read' });
|
|
231
|
+
const url = avatars.presignPut({ userId }, { acl: 'private', expiresIn: 300 });
|
|
197
232
|
```
|
|
198
233
|
|
|
234
|
+
### `expiresIn is seconds, and must be above 0 and at most 604800 (seven days, which is S3's own limit); got 1000000000000`
|
|
235
|
+
|
|
236
|
+
**When:** `presignGet` or `presignPut` with an `expiresIn` that is not a
|
|
237
|
+
finite number of seconds inside S3's range.
|
|
238
|
+
**Why:** an `S3Error` with `code: 'WRONG_OPTION'`, raised before anything is
|
|
239
|
+
signed. Measured on bun 1.4.2, the client refuses `0` and below with a
|
|
240
|
+
`TypeError` of its own and **signs** `1e12` happily — a URL the service then
|
|
241
|
+
rejects when somebody uses it, long after this package reported success.
|
|
242
|
+
**Fix:** pass seconds, and no more than a week:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const url = avatars.presignPut({ userId }, { expiresIn: 300 }); // five minutes
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Sign for the time the page actually needs: a day is a long life for a URL
|
|
249
|
+
anyone can forward.
|
|
250
|
+
|
|
199
251
|
## The service
|
|
200
252
|
|
|
201
253
|
### `Missing S3 credentials. 'accessKeyId', 'secretAccessKey', 'bucket', and 'endpoint' are required`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/s3",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|