@nxgt/s3 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # @nxgt/s3
2
+
3
+ S3 on **Bun's own client** — no AWS SDK. A bucket is described once: which
4
+ bucket, how a key is built from the things that identify an object, and what
5
+ this application is willing to put there. Writes that break those rules are
6
+ refused **before anything is sent**, and presigned URLs come from the same
7
+ definition.
8
+
9
+ ```ts
10
+ import { bindBucket, defineBucket } from '@nxgt/s3';
11
+
12
+ export const avatars = defineBucket({
13
+ bucket: 'avatars',
14
+ key: (p: { userId: string }) => `${p.userId}.png`,
15
+ contentType: ['image/png', 'image/jpeg'],
16
+ maxSize: 2 * 1024 * 1024, // bytes
17
+ });
18
+
19
+ const store = bindBucket(avatars, {
20
+ endpoint: process.env.S3_ENDPOINT,
21
+ accessKeyId: process.env.S3_KEY,
22
+ secretAccessKey: process.env.S3_SECRET,
23
+ });
24
+
25
+ await store.put({ userId: 'u1' }, png, { type: 'image/png' });
26
+ const url = store.presignGet({ userId: 'u1' }, { expiresIn: 300 }); // seconds
27
+ ```
28
+
29
+ `store.client` is Bun's `S3Client`, and `store.file(params)` its `S3File`:
30
+ everything this package does not wrap is still there.
31
+
32
+ > **0.x, on Bun's `S3Client`.** The API is still settling.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ bun add @nxgt/s3 typescript
38
+ ```
39
+
40
+ - **Bun 1.4 or later, and Bun only.** `S3Client` is built into Bun, which is
41
+ why there is no SDK to install — and why this package does not run on Node.
42
+ - `typescript` `^6.0.3`: required peer, the version every `@nxgt` package pins.
43
+ - Tested against SeaweedFS 4.47's S3 gateway. Anything S3-compatible that Bun
44
+ can sign for will do; a service that is not AWS wants
45
+ `virtualHostedStyle: false`.
46
+
47
+ ## What it does not do
48
+
49
+ - **No multipart upload.** A body goes in one `put`. Bun's own
50
+ `file.writer()` is there for the rest, through `store.file(params)`.
51
+ - **No bucket administration.** Creating, deleting and configuring a bucket is
52
+ not this package's, and Bun's client has no `createBucket` either.
53
+ - **No copy or move.** `store.client` is where those live when Bun grows them.
54
+ - **It adds no retry and no cache of its own.** A failed request comes back
55
+ with S3's own error. Bun's client does retry — `retry`, three attempts by
56
+ default — and that option is passed through like the rest.
57
+
58
+ ## API
59
+
60
+ ### `defineBucket({ bucket, key, contentType, maxSize })`
61
+
62
+ Describes a bucket; it talks to nothing, so the definition is what a server
63
+ and a worker share. The key is a **function**, so nothing is spelled by hand
64
+ at a call site and a renamed parameter is a compile error.
65
+
66
+ | Field | |
67
+ | --- | --- |
68
+ | `bucket` | the bucket's name on the service |
69
+ | `key` | `(params) => string`, the object's key |
70
+ | `contentType` | a type, or a list of them, that this bucket accepts. Left out, anything goes |
71
+ | `maxSize` | the biggest body in **bytes**. Left out, anything goes |
72
+
73
+ ### `bindBucket(definition, options?)`
74
+
75
+ `options` is Bun's own `S3Options` without `bucket`, passed through whole —
76
+ `endpoint`, `accessKeyId`, `secretAccessKey`, `region`, `sessionToken`,
77
+ `virtualHostedStyle`, and also `acl`, `storageClass`, `retry`, `partSize` and
78
+ `queueSize`. It is optional: given none, Bun reads its own `S3_*` / `AWS_*`
79
+ environment variables. Each bound bucket holds an `S3Client` of its own: S3 is
80
+ stateless HTTP, so there is no connection to share and nothing to close.
81
+
82
+ | `BoundBucket<P>` | |
83
+ | --- | --- |
84
+ | `client` | the `S3Client` this holds |
85
+ | `keyFor(params)` | the key it would use |
86
+ | `file(params)` | Bun's lazy `S3File`: `.stream()`, `.slice()`, `.writer()` |
87
+ | `put(params, body, options?)` | writes it, once the content type and size have accepted it. `options.type` names the body's content type; a `Blob` is asked for its own when none is given |
88
+ | `bytes(params)` | the bytes, or `undefined` when there is no such object |
89
+ | `text(params)` | the body as text, or `undefined` |
90
+ | `exists(params)` | |
91
+ | `stat(params)` | Bun's `S3Stats` — `size`, `lastModified`, `etag`, `type` — or `undefined`. Note `etag`, lower case: that is Bun's field. A listing's `StoredObject` says `eTag` |
92
+ | `delete(params)` | |
93
+ | `list({ prefix, limit, cursor })` | one `ObjectPage` |
94
+ | `presignGet(params, { expiresIn, acl })` | a signed URL that reads it |
95
+ | `presignPut(params, { expiresIn, acl })` | a signed URL that writes it. It takes no `type` — see the Traps |
96
+
97
+ ### Listing
98
+
99
+ ```ts
100
+ let cursor: string | null = null;
101
+ do {
102
+ const page = await store.list({ prefix: 'u1/', limit: 100, cursor });
103
+ for (const object of page.items) console.log(object.key, object.size);
104
+ cursor = page.nextCursor;
105
+ } while (cursor !== null);
106
+ ```
107
+
108
+ `ObjectPage` is the same shape as the `CursorPage` of `@nxgt/drizzle` and
109
+ `@nxgt/mongo`, so a caller pages the same way everywhere: `items`, and a
110
+ `nextCursor` that is `null` on the last page. S3's `continuationToken` is what
111
+ it carries.
112
+
113
+ ### Types
114
+
115
+ | Type | |
116
+ | --- | --- |
117
+ | `BucketDefinition<P>` | what `defineBucket` takes and gives back |
118
+ | `BoundBucket<P>` | what `bindBucket` gives back |
119
+ | `ObjectPage` | `{ items: StoredObject[]; nextCursor: string \| null }` |
120
+ | `StoredObject` | `{ key: string; size: number \| undefined; lastModified: Date \| undefined; eTag: string \| undefined }` — S3 does not promise the last three, so they are optional here |
121
+ | `PresignOptions` | `{ expiresIn?: number; acl?: … }` |
122
+ | `ParamsOf<D>` | what a definition's `key` takes, for a caller writing its own helper |
123
+ | `PutBody` | everything Bun's `write` takes |
124
+
125
+ `stat` gives back Bun's own `S3Stats`, which this package does not re-export;
126
+ import it from `bun` where you need to name it.
127
+
128
+ ## Errors
129
+
130
+ `S3Error` is what this package throws, and **every one of them is thrown
131
+ before anything is sent**. It carries a `code` and the object `key` — never
132
+ the body, never a credential.
133
+
134
+ ```ts
135
+ if (error instanceof S3Error && error.code === 'TOO_LARGE') {
136
+ return c.json({ error: 'That file is too big' }, 413);
137
+ }
138
+ ```
139
+
140
+ | `S3ErrorCode` | |
141
+ | --- | --- |
142
+ | `WRONG_TYPE` | the body's content type is not one this bucket accepts — or the write named none and the bucket names some |
143
+ | `TOO_LARGE` | the body is bigger than `maxSize` |
144
+ | `UNMEASURABLE` | `maxSize` is set and the body's size cannot be known before sending |
145
+
146
+ `defineBucket` throws a `TypeError` for a definition that could never work: an
147
+ empty `bucket`, a `maxSize` that is not a positive number, an empty list of
148
+ content types. S3's own failures come back as they are, from Bun's client —
149
+ and **Bun names those `S3Error` too**, with S3's codes (`NoSuchKey` and the
150
+ rest). Discriminate with `instanceof S3Error` on this package's class, never
151
+ with `error.name`.
152
+
153
+ ## What does not compile
154
+
155
+ Each is a `@ts-expect-error` case in `test/types/s3.ts`.
156
+
157
+ - An object read, written, deleted or signed for with the wrong key
158
+ parameters, and a `put` given a misspelt option.
159
+ - A `presignPut` given a `type`: a presigned PUT constrains no content type,
160
+ so it takes none.
161
+ - A definition with no `bucket`, no `key`, a `key` that gives something other
162
+ than a string, a `maxSize` that is not a number, or a misspelt option.
163
+ - A presigned URL asked for without the params that identify the object, or
164
+ with an `expiresIn` that is not a number.
165
+ - A listing asked for by page number, or a page read for a `total` — it pages
166
+ by cursor, and S3 does not count.
167
+
168
+ ## Traps
169
+
170
+ - **The guards are `put`'s, not the bucket's.** `contentType` and `maxSize`
171
+ are checked here, in `put`, before the request goes out. They cost nothing
172
+ and they catch the honest mistake, but nothing else on the object goes
173
+ through them: `store.file(params).writer()` and `store.client` are Bun's own
174
+ and write whatever they are given, and **anyone holding a presigned PUT can
175
+ ignore them entirely**. Set the service's own policy too where it matters.
176
+ - **A presigned PUT constrains the key and the deadline, and nothing else.**
177
+ Not the size, not the content type — measured on Bun 1.4: `presign`'s `type`
178
+ only adds `response-content-type`, which is S3's override for what a
179
+ *download* is labelled, and `X-Amz-SignedHeaders` stays `host`, so the
180
+ uploader's `Content-Type` is never signed. A URL signed for a `text/csv`
181
+ bucket stores a zip happily. That is why `presignPut` takes no `type` at
182
+ all: it would read as a guarantee it cannot make. Check with `stat` after
183
+ the upload, or enforce it in the bucket's own policy.
184
+ - **A body whose size cannot be known is refused, not streamed.** With
185
+ `maxSize` set, a `Response`, a `Request` or another `S3File` throws
186
+ `UNMEASURABLE`: nothing can check a length it has not read — an `S3File`
187
+ reports its size as `NaN` until the service has been asked. Read it into
188
+ memory first, or leave `maxSize` out and let the service refuse it.
189
+ - **A content type is compared on its essence.** `text/csv` accepts
190
+ `text/csv;charset=utf-8` and `TEXT/CSV`, because that is what real bodies
191
+ carry: `Bun.file('a.csv').type` is `text/csv;charset=utf-8`, and a text
192
+ `Blob` adds the charset by itself. Parameters and case are ignored; nothing
193
+ else is.
194
+ - **A string body is measured in bytes, not in characters**, and `maxSize` is
195
+ inclusive: 1024 passes, 1025 does not.
196
+ - **`expiresIn` is seconds, and Bun's default is a day.** Always pass one.
197
+ - **A key is built, never guessed.** `keyFor` is there so a caller that needs
198
+ the string gets *the* string; building one by hand somewhere else is how a
199
+ bucket ends up with two spellings of the same object.
200
+ - **`delete` does not say whether anything was there.** S3 answers the same
201
+ either way, and this package does not pretend otherwise. Call `exists`
202
+ first when it matters.
203
+ - **A listing is eventually consistent on some services**, and `limit` is a
204
+ maximum, not a promise: a page may come back shorter with a `nextCursor`
205
+ still set. Page until `nextCursor` is `null`, never until a page is short.
206
+ - **`undefined` means "no such object", and nothing else.** `bytes`, `text`
207
+ and `stat` read in one round trip and turn S3's own `NoSuchKey` into
208
+ `undefined`; every other failure — a wrong secret, a refused request, a
209
+ service that is down — comes back as the error it is.
210
+
211
+ ## License
212
+
213
+ MIT
@@ -0,0 +1,58 @@
1
+ import { S3Client, type S3File, type S3Options, type S3Stats } from 'bun';
2
+ import { type PresignOptions } from './operations/presign';
3
+ import type { BucketDefinition, ObjectPage, PutBody } from './types';
4
+ export type { PresignOptions };
5
+ /** A bucket bound to credentials: the definition, with somewhere to put it. */
6
+ export interface BoundBucket<P> {
7
+ /** The client this holds, for anything this package does not wrap. */
8
+ readonly client: S3Client;
9
+ /** The key this would use, for a caller that needs the string itself. */
10
+ keyFor(params: P): string;
11
+ /** Bun's own lazy handle: `.stream()`, `.slice()`, `.writer()` and the rest. */
12
+ file(params: P): S3File;
13
+ /** Writes it, once the bucket's content type and size have accepted it. */
14
+ put(params: P, body: PutBody, options?: {
15
+ type?: string;
16
+ }): Promise<void>;
17
+ /** The bytes, or `undefined` when there is no such object. */
18
+ bytes(params: P): Promise<Uint8Array | undefined>;
19
+ /** The body as text, or `undefined` when there is no such object. */
20
+ text(params: P): Promise<string | undefined>;
21
+ exists(params: P): Promise<boolean>;
22
+ /** What the service knows about it, or `undefined` when it is not there. */
23
+ stat(params: P): Promise<S3Stats | undefined>;
24
+ /** Removes it. S3 does not say whether anything was there, and nor does this. */
25
+ delete(params: P): Promise<void>;
26
+ /** One page of the bucket, in this repository's cursor shape. */
27
+ list(options?: {
28
+ prefix?: string;
29
+ limit?: number;
30
+ cursor?: string | null;
31
+ }): Promise<ObjectPage>;
32
+ /** A URL that reads this object, signed. */
33
+ presignGet(params: P, options?: PresignOptions): string;
34
+ /**
35
+ * A URL that writes this object, signed. It constrains the key and the
36
+ * deadline, and **nothing else** — not the size, not the content type.
37
+ */
38
+ presignPut(params: P, options?: PresignOptions): string;
39
+ }
40
+ /**
41
+ * Binds a bucket definition to credentials.
42
+ *
43
+ * ```ts
44
+ * const store = bindBucket(avatars, {
45
+ * endpoint: process.env.S3_ENDPOINT,
46
+ * accessKeyId: process.env.S3_KEY,
47
+ * secretAccessKey: process.env.S3_SECRET,
48
+ * });
49
+ * await store.put({ userId: 'u1' }, png, { type: 'image/png' });
50
+ * ```
51
+ *
52
+ * Given no options, Bun reads its own `S3_*` / `AWS_*` environment variables.
53
+ *
54
+ * Each bound bucket holds an `S3Client` of its own. S3 is stateless HTTP —
55
+ * there is no connection to share, and nothing to close.
56
+ */
57
+ export declare function bindBucket<P>(definition: BucketDefinition<P>, options?: Omit<S3Options, 'bucket'>): BoundBucket<P>;
58
+ //# sourceMappingURL=bind-bucket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-bucket.d.ts","sourceRoot":"","sources":["../../src/bucket/bind-bucket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,KAAK,CAAC;AAG1E,OAAO,EACN,KAAK,cAAc,EAGnB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAErE,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B,+EAA+E;AAC/E,MAAM,WAAW,WAAW,CAAC,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,yEAAyE;IACzE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC;IAC1B,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC;IACxB,2EAA2E;IAC3E,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,8DAA8D;IAC9D,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAClD,qEAAqE;IACrE,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,4EAA4E;IAC5E,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9C,iFAAiF;IACjF,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,iEAAiE;IACjE,IAAI,CAAC,OAAO,CAAC,EAAE;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACxB,4CAA4C;IAC5C,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC;IACxD;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC;CACxD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAC3B,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,OAAO,GAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAM,GACrC,WAAW,CAAC,CAAC,CAAC,CAoBhB"}
@@ -0,0 +1,24 @@
1
+ import type { S3Client } from 'bun';
2
+ import type { BucketDefinition } from './types';
3
+ /**
4
+ * What every operation of a bound bucket works from, resolved once: the
5
+ * client, the definition, and the accepted content types as a list.
6
+ *
7
+ * It holds **data only**. The operations are plain functions that take it as
8
+ * their first argument, in `guards.ts` and `operations/` — a context of
9
+ * closures would only be the factory this package split up, one size down.
10
+ */
11
+ export interface BucketContext<P> {
12
+ readonly client: S3Client;
13
+ readonly definition: BucketDefinition<P>;
14
+ /**
15
+ * The types the definition accepts, always as a list, **as written** —
16
+ * an error message quotes these, and the comparison normalises them.
17
+ * `undefined` when the bucket accepts anything.
18
+ */
19
+ readonly accepted: readonly string[] | undefined;
20
+ }
21
+ export declare function bucketContext<P>(client: S3Client, definition: BucketDefinition<P>): BucketContext<P>;
22
+ /** The key this definition builds for these parameters. */
23
+ export declare function keyOf<P>(context: BucketContext<P>, params: P): string;
24
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/bucket/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CACjD;AAUD,wBAAgB,aAAa,CAAC,CAAC,EAC9B,MAAM,EAAE,QAAQ,EAChB,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC7B,aAAa,CAAC,CAAC,CAAC,CAMlB;AAED,2DAA2D;AAC3D,wBAAgB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAErE"}
@@ -0,0 +1,16 @@
1
+ import type { BucketDefinition } from './types';
2
+ /**
3
+ * Describes a bucket. It talks to nothing: `bindBucket` is what needs
4
+ * credentials.
5
+ *
6
+ * ```ts
7
+ * export const avatars = defineBucket({
8
+ * bucket: 'avatars',
9
+ * key: (p: { userId: string }) => `${p.userId}.png`,
10
+ * contentType: ['image/png', 'image/jpeg'],
11
+ * maxSize: 2 * 1024 * 1024,
12
+ * });
13
+ * ```
14
+ */
15
+ export declare function defineBucket<P>(definition: BucketDefinition<P>): BucketDefinition<P>;
16
+ //# sourceMappingURL=define-bucket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-bucket.d.ts","sourceRoot":"","sources":["../../src/bucket/define-bucket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC7B,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC7B,gBAAgB,CAAC,CAAC,CAAC,CAsBrB"}
@@ -0,0 +1,25 @@
1
+ import type { BucketContext } from './context';
2
+ import type { PutBody } from './types';
3
+ /**
4
+ * A content type without its parameters, lower-cased: `text/plain` from
5
+ * `text/plain;charset=utf-8`.
6
+ *
7
+ * Both sides of the comparison go through this, because the type a body
8
+ * carries is rarely the bare one a definition names — `Bun.file('a.json')`
9
+ * reports `application/json;charset=utf-8` (measured on bun 1.4.2), and an
10
+ * explicit `IMAGE/PNG` is the same type as `image/png`.
11
+ */
12
+ export declare function essenceOf(type: string): string;
13
+ /**
14
+ * The type a write would carry: the one the caller named, or the one the
15
+ * body knows about itself. One function, so the type `check` approves and
16
+ * the type `put` sends can never be two different answers.
17
+ */
18
+ export declare function effectiveType(body: PutBody, named: string | undefined): string | undefined;
19
+ /** The body's size, or `undefined` when it cannot be known before sending. */
20
+ export declare function sizeOf(body: PutBody): number | undefined;
21
+ /** Refuses a type the bucket does not accept. Shared by `put` and `presign`. */
22
+ export declare function checkType<P>(context: BucketContext<P>, key: string, type: string | undefined): void;
23
+ /** Refuses a body the bucket does not accept, before anything is sent. */
24
+ export declare function checkSize<P>(context: BucketContext<P>, key: string, body: PutBody): void;
25
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +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;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;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"}
@@ -0,0 +1,9 @@
1
+ import type { BucketContext } from '../context';
2
+ import type { ObjectPage } from '../types';
3
+ /** One page of the bucket, in this repository's cursor shape. */
4
+ export declare function listObjects<P>(context: BucketContext<P>, options?: {
5
+ prefix?: string;
6
+ limit?: number;
7
+ cursor?: string | null;
8
+ }): Promise<ObjectPage>;
9
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/list.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,UAAU,CAAC;AAEzD,iEAAiE;AACjE,wBAAsB,WAAW,CAAC,CAAC,EAClC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACvE,OAAO,CAAC,UAAU,CAAC,CAsBrB"}
@@ -0,0 +1,23 @@
1
+ import type { S3Options } from 'bun';
2
+ import { type BucketContext } from '../context';
3
+ /** How a presigned URL is asked for. `expiresIn` is **seconds**, as S3's is. */
4
+ export interface PresignOptions {
5
+ /** Seconds until it expires. Bun's default is a day; give one. */
6
+ expiresIn?: number;
7
+ /** `public-read` and the rest, when the service honours it. */
8
+ acl?: S3Options['acl'];
9
+ }
10
+ export declare function presignGetUrl<P>(context: BucketContext<P>, params: P, options?: PresignOptions): string;
11
+ /**
12
+ * A URL that writes this object, signed.
13
+ *
14
+ * It carries **no content type**, and takes none. Measured on bun 1.4.2:
15
+ * `presign`'s `type` only adds `response-content-type`, S3's override for
16
+ * what a *download* is labelled; `X-Amz-SignedHeaders` stays `host`, so the
17
+ * `Content-Type` the uploader sends is not signed and not constrained. A PUT
18
+ * signed for a `text/csv` bucket stores an `application/zip` body happily —
19
+ * measured against this package's own test service, status 200. Naming a type
20
+ * here would only look like a guarantee.
21
+ */
22
+ export declare function presignPutUrl<P>(context: BucketContext<P>, params: P, options?: PresignOptions): string;
23
+ //# sourceMappingURL=presign.d.ts.map
@@ -0,0 +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;AAEvD,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;AAED,wBAAgB,aAAa,CAAC,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE,cAAc,GACtB,MAAM,CAKR;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,CAKR"}
@@ -0,0 +1,7 @@
1
+ import type { S3Stats } from 'bun';
2
+ import { type BucketContext } from '../context';
3
+ export declare function readBytes<P>(context: BucketContext<P>, params: P): Promise<Uint8Array | undefined>;
4
+ export declare function readText<P>(context: BucketContext<P>, params: P): Promise<string | undefined>;
5
+ export declare function statObject<P>(context: BucketContext<P>, params: P): Promise<S3Stats | undefined>;
6
+ export declare function objectExists<P>(context: BucketContext<P>, params: P): Promise<boolean>;
7
+ //# sourceMappingURL=reads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reads.d.ts","sourceRoot":"","sources":["../../../src/bucket/operations/reads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,KAAK,CAAC;AAC3C,OAAO,EAAE,KAAK,aAAa,EAAS,MAAM,YAAY,CAAC;AAwBvD,wBAAgB,SAAS,CAAC,CAAC,EAC1B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAEjC;AAED,wBAAgB,QAAQ,CAAC,CAAC,EACzB,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED,wBAAgB,UAAU,CAAC,CAAC,EAC3B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAE9B;AAED,wBAAgB,YAAY,CAAC,CAAC,EAC7B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,OAAO,CAAC,CAElB"}
@@ -0,0 +1,12 @@
1
+ import { type BucketContext } from '../context';
2
+ import type { PutBody } from '../types';
3
+ /**
4
+ * Writes it, once the bucket's content type and size have accepted it. Both
5
+ * guards run before `write` is called, so a refused body is never sent.
6
+ */
7
+ export declare function putObject<P>(context: BucketContext<P>, params: P, body: PutBody, options?: {
8
+ type?: string;
9
+ }): Promise<void>;
10
+ /** Removes it. S3 does not say whether anything was there, and nor does this. */
11
+ export declare function deleteObject<P>(context: BucketContext<P>, params: P): Promise<void>;
12
+ //# sourceMappingURL=writes.d.ts.map
@@ -0,0 +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,MAAM,UAAU,CAAC;AAExC;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAChC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,CAAC,EAC7B,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACP,OAAO,CAAC,IAAI,CAAC,CAEf"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * One bucket: which one it is, how an object's key is built from the things
3
+ * that identify it, and what this application is willing to put in it.
4
+ *
5
+ * The key is a **function**, not a template, so nothing is spelled by hand at
6
+ * a call site and a renamed parameter is a compile error.
7
+ */
8
+ export interface BucketDefinition<P> {
9
+ /** The bucket's name on the service. */
10
+ readonly bucket: string;
11
+ /** The object's key, from whatever identifies it. */
12
+ readonly key: (params: P) => string;
13
+ /**
14
+ * The content types this bucket accepts. A write of anything else is
15
+ * refused **before it is sent**. Left out, anything goes.
16
+ */
17
+ readonly contentType?: string | readonly string[];
18
+ /**
19
+ * The biggest body this bucket accepts, in bytes, refused before it is
20
+ * sent. Left out, anything goes — see the Traps about bodies whose size
21
+ * cannot be known in advance.
22
+ */
23
+ readonly maxSize?: number;
24
+ }
25
+ /** What a listing says about one object. */
26
+ export interface StoredObject {
27
+ key: string;
28
+ /** Bytes, or `undefined` when the service did not say. */
29
+ size: number | undefined;
30
+ lastModified: Date | undefined;
31
+ eTag: string | undefined;
32
+ }
33
+ /**
34
+ * One page of a listing. The same shape as the `CursorPage` of
35
+ * `@nxgt/drizzle` and `@nxgt/mongo`, so a caller pages the same way here —
36
+ * S3's `continuationToken` is what `nextCursor` carries.
37
+ */
38
+ export interface ObjectPage {
39
+ items: StoredObject[];
40
+ /** Pass it as `cursor` for the next page; `null` on the last one. */
41
+ nextCursor: string | null;
42
+ }
43
+ /** What a definition's `key` takes, so a caller can write its own helper. */
44
+ export type ParamsOf<D> = D extends BucketDefinition<infer P> ? P : never;
45
+ import type { S3Client } from 'bun';
46
+ /**
47
+ * Everything Bun's own `write` takes — a string, bytes, a `Blob`, a stream, a
48
+ * `Response`. A bucket with a `maxSize` refuses the ones whose size cannot be
49
+ * known before sending; one without accepts them all.
50
+ */
51
+ export type PutBody = Parameters<S3Client['write']>[1];
52
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bucket/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAClC,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,YAAY,EAAE,IAAI,GAAG,SAAS,CAAC;IAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,qEAAqE;IACrE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE1E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ /** What went wrong. Each one is documented in the README's Traps. */
2
+ export type S3ErrorCode =
3
+ /** The body's content type is not one this bucket accepts. */
4
+ 'WRONG_TYPE'
5
+ /** The body is bigger than this bucket's `maxSize`. */
6
+ | 'TOO_LARGE'
7
+ /** The body's size cannot be known before sending, and `maxSize` is set. */
8
+ | 'UNMEASURABLE';
9
+ /**
10
+ * This package's only error, and every one of them is thrown **before**
11
+ * anything is sent. S3's own failures come back as they are, from Bun's
12
+ * client.
13
+ */
14
+ export declare class S3Error extends Error {
15
+ readonly code: S3ErrorCode;
16
+ /** The object key it was about — the bucket and the key, never the body. */
17
+ readonly key: string;
18
+ constructor(code: S3ErrorCode, key: string, message: string);
19
+ }
20
+ //# sourceMappingURL=s3-error.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,7 @@
1
+ export type { BoundBucket, PresignOptions } from './bucket/bind-bucket';
2
+ export { bindBucket } from './bucket/bind-bucket';
3
+ export { defineBucket } from './bucket/define-bucket';
4
+ export type { BucketDefinition, ObjectPage, ParamsOf, PutBody, StoredObject, } from './bucket/types';
5
+ export type { S3ErrorCode } from './errors/s3-error';
6
+ export { S3Error } from './errors/s3-error';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,YAAY,EACX,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,YAAY,GACZ,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,185 @@
1
+ // src/bucket/bind-bucket.ts
2
+ import { S3Client } from "bun";
3
+
4
+ // src/bucket/context.ts
5
+ function acceptedTypes(contentType) {
6
+ if (contentType === undefined)
7
+ return;
8
+ return typeof contentType === "string" ? [contentType] : contentType;
9
+ }
10
+ function bucketContext(client, definition) {
11
+ return {
12
+ client,
13
+ definition,
14
+ accepted: acceptedTypes(definition.contentType)
15
+ };
16
+ }
17
+ function keyOf(context, params) {
18
+ return context.definition.key(params);
19
+ }
20
+
21
+ // src/bucket/operations/list.ts
22
+ async function listObjects(context, options = {}) {
23
+ const answer = await context.client.list({
24
+ prefix: options.prefix,
25
+ maxKeys: options.limit,
26
+ continuationToken: options.cursor ?? undefined
27
+ });
28
+ const contents = answer.contents ?? [];
29
+ const items = contents.map((found) => ({
30
+ key: found.key,
31
+ size: found.size,
32
+ lastModified: found.lastModified ? new Date(found.lastModified) : undefined,
33
+ eTag: found.eTag
34
+ }));
35
+ const next = answer.isTruncated ? answer.nextContinuationToken ?? null : null;
36
+ return { items, nextCursor: next };
37
+ }
38
+
39
+ // src/bucket/operations/presign.ts
40
+ function presignGetUrl(context, params, options) {
41
+ return context.client.presign(keyOf(context, params), {
42
+ ...options,
43
+ method: "GET"
44
+ });
45
+ }
46
+ function presignPutUrl(context, params, options) {
47
+ return context.client.presign(keyOf(context, params), {
48
+ ...options,
49
+ method: "PUT"
50
+ });
51
+ }
52
+
53
+ // src/bucket/operations/reads.ts
54
+ async function whenPresent(context, params, read) {
55
+ try {
56
+ return await read(context.client.file(keyOf(context, params)));
57
+ } catch (reason) {
58
+ if (reason.code === "NoSuchKey")
59
+ return;
60
+ throw reason;
61
+ }
62
+ }
63
+ function readBytes(context, params) {
64
+ return whenPresent(context, params, (file) => file.bytes());
65
+ }
66
+ function readText(context, params) {
67
+ return whenPresent(context, params, (file) => file.text());
68
+ }
69
+ function statObject(context, params) {
70
+ return whenPresent(context, params, (file) => file.stat());
71
+ }
72
+ function objectExists(context, params) {
73
+ return context.client.exists(keyOf(context, params));
74
+ }
75
+
76
+ // src/errors/s3-error.ts
77
+ class S3Error extends Error {
78
+ constructor(code, key, message) {
79
+ super(message);
80
+ this.name = "S3Error";
81
+ this.code = code;
82
+ this.key = key;
83
+ }
84
+ }
85
+
86
+ // src/bucket/guards.ts
87
+ function essenceOf(type) {
88
+ return (type.split(";")[0] ?? "").trim().toLowerCase();
89
+ }
90
+ function effectiveType(body, named) {
91
+ return named ?? (body instanceof Blob ? body.type || undefined : undefined);
92
+ }
93
+ function sizeOf(body) {
94
+ if (typeof body === "string")
95
+ return Buffer.byteLength(body, "utf8");
96
+ if (body instanceof Blob) {
97
+ return Number.isFinite(body.size) ? body.size : undefined;
98
+ }
99
+ if (body instanceof ArrayBuffer)
100
+ return body.byteLength;
101
+ if (ArrayBuffer.isView(body))
102
+ return body.byteLength;
103
+ return;
104
+ }
105
+ function checkType(context, key, type) {
106
+ const { accepted } = context;
107
+ if (!accepted)
108
+ return;
109
+ const list = accepted.join(", ");
110
+ if (!type) {
111
+ throw new S3Error("WRONG_TYPE", key, `"${context.definition.bucket}" accepts ${list}, and this write ` + "names no content type. Pass `type`");
112
+ }
113
+ if (!accepted.some((one) => essenceOf(one) === essenceOf(type))) {
114
+ throw new S3Error("WRONG_TYPE", key, `"${context.definition.bucket}" accepts ${list}, not ${type}`);
115
+ }
116
+ }
117
+ function checkSize(context, key, body) {
118
+ const { maxSize, bucket } = context.definition;
119
+ if (maxSize === undefined)
120
+ return;
121
+ const size = sizeOf(body);
122
+ if (size === undefined) {
123
+ throw new S3Error("UNMEASURABLE", key, `"${bucket}" has a maxSize, and this body's size cannot be known ` + "before sending it. Read it into memory first, or drop `maxSize` " + "and let the service refuse it");
124
+ }
125
+ if (size > maxSize) {
126
+ throw new S3Error("TOO_LARGE", key, `"${bucket}" accepts ${maxSize} bytes at most, and this body is ${size}`);
127
+ }
128
+ }
129
+
130
+ // src/bucket/operations/writes.ts
131
+ async function putObject(context, params, body, options) {
132
+ const key = keyOf(context, params);
133
+ const type = effectiveType(body, options?.type);
134
+ checkType(context, key, type);
135
+ checkSize(context, key, body);
136
+ await context.client.write(key, body, type ? { type } : undefined);
137
+ }
138
+ function deleteObject(context, params) {
139
+ return context.client.delete(keyOf(context, params));
140
+ }
141
+
142
+ // src/bucket/bind-bucket.ts
143
+ function bindBucket(definition, options = {}) {
144
+ const client = new S3Client({ ...options, bucket: definition.bucket });
145
+ const context = bucketContext(client, definition);
146
+ return {
147
+ client,
148
+ keyFor: (params) => keyOf(context, params),
149
+ file: (params) => client.file(keyOf(context, params)),
150
+ put: (params, body, putOptions) => putObject(context, params, body, putOptions),
151
+ bytes: (params) => readBytes(context, params),
152
+ text: (params) => readText(context, params),
153
+ exists: (params) => objectExists(context, params),
154
+ stat: (params) => statObject(context, params),
155
+ delete: (params) => deleteObject(context, params),
156
+ list: (listOptions) => listObjects(context, listOptions),
157
+ presignGet: (params, presignOptions) => presignGetUrl(context, params, presignOptions),
158
+ presignPut: (params, presignOptions) => presignPutUrl(context, params, presignOptions)
159
+ };
160
+ }
161
+ // src/bucket/define-bucket.ts
162
+ function defineBucket(definition) {
163
+ if (definition.bucket.length === 0) {
164
+ throw new TypeError("defineBucket: a bucket definition needs a bucket");
165
+ }
166
+ if (definition.maxSize !== undefined) {
167
+ const { maxSize } = definition;
168
+ if (!Number.isFinite(maxSize) || maxSize <= 0) {
169
+ throw new TypeError(`defineBucket: "${definition.bucket}" has a maxSize of ${maxSize}; ` + "it is a number of bytes, and must be above zero");
170
+ }
171
+ }
172
+ const types = definition.contentType;
173
+ if (Array.isArray(types) && types.length === 0) {
174
+ throw new TypeError(`defineBucket: "${definition.bucket}" accepts an empty list of ` + "content types, so nothing could ever be written. Leave " + "`contentType` out to accept anything");
175
+ }
176
+ return Object.freeze({ ...definition });
177
+ }
178
+ export {
179
+ S3Error,
180
+ bindBucket,
181
+ defineBucket
182
+ };
183
+
184
+ //# debugId=4049B661AFFFECB264756E2164756E21
185
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/bucket/bind-bucket.ts", "../src/bucket/context.ts", "../src/bucket/operations/list.ts", "../src/bucket/operations/presign.ts", "../src/bucket/operations/reads.ts", "../src/errors/s3-error.ts", "../src/bucket/guards.ts", "../src/bucket/operations/writes.ts", "../src/bucket/define-bucket.ts"],
4
+ "sourcesContent": [
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 { BucketDefinition, ObjectPage, PutBody } 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/** Writes it, once the bucket's content type and size have accepted it. */\n\tput(params: P, body: PutBody, options?: { type?: string }): 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
+ "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
+ "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
+ "import type { S3Options } from 'bun';\nimport { type BucketContext, keyOf } from '../context';\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\nexport function presignGetUrl<P>(\n\tcontext: BucketContext<P>,\n\tparams: P,\n\toptions?: PresignOptions,\n): string {\n\treturn context.client.presign(keyOf(context, params), {\n\t\t...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\treturn context.client.presign(keyOf(context, params), {\n\t\t...options,\n\t\tmethod: 'PUT',\n\t});\n}\n",
9
+ "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
+ "/** 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\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",
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 } 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?: { type?: string },\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.\n\tawait context.client.write(key, body, type ? { type } : undefined);\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
+ "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
+ ],
15
+ "mappings": "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",
16
+ "debugId": "4049B661AFFFECB264756E2164756E21",
17
+ "names": []
18
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@nxgt/s3",
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "package.json",
13
+ "LICENSE"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "keywords": [
24
+ "s3",
25
+ "bun",
26
+ "typescript",
27
+ "storage",
28
+ "presigned-url"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/softistx/nxgt-data.git",
33
+ "directory": "packages/s3"
34
+ },
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org",
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "bun run ../../build.ts",
41
+ "test": "bun run ../../scripts/seaweedfs.ts && bun test --timeout 30000 src",
42
+ "typecheck": "tsc --noEmit"
43
+ },
44
+ "nxgt": {
45
+ "entrypoints": [
46
+ "src/index.ts"
47
+ ]
48
+ },
49
+ "devDependencies": {
50
+ "@types/bun": "^1.4.0"
51
+ },
52
+ "peerDependencies": {
53
+ "typescript": "^6.0.3"
54
+ }
55
+ }