@stowage/core 0.0.0 → 0.2.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 Alexander Kaufmann
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,120 @@
1
+ # @stowage/core
2
+
3
+ The types every stowage adapter implements, `StorageError`, and the utilities an adapter calls. It
4
+ does nothing without an adapter.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install @stowage/core
10
+ ```
11
+
12
+ ## Example
13
+
14
+ A function written against `Storage` runs against every adapter: `memoryStorage()` in a test,
15
+ `fsStorage()` on a laptop, `s3Storage()` or `azureBlobStorage()` in production.
16
+
17
+ ```ts
18
+ import { isStorageError, type Storage } from "@stowage/core";
19
+
20
+ export async function readSettings(storage: Storage): Promise<unknown> {
21
+ try {
22
+ const object = await storage.get("settings.json");
23
+
24
+ return await object.json();
25
+ } catch (failure) {
26
+ if (isStorageError(failure) && failure.code === "NotFound") return {};
27
+
28
+ throw failure;
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Runtimes
34
+
35
+ Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01` without Node APIs. CI last ran green on Bun 1.4.2 and Deno 2.9.6.
36
+
37
+ The bundle measures 3.3 kB minified and gzipped.
38
+
39
+ ## Limits
40
+
41
+ This section is empty. `@stowage/core` declares no capability; each storage declares its own, out
42
+ of the five names of [spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#49-capabilities).
43
+
44
+ ## Notes
45
+
46
+ A failure reaches the caller in one of two shapes: a `StorageError`, which `isStorageError` tells
47
+ apart, or the runtime's `AbortError` once a signal fired
48
+ ([spec 4.10](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#410-errors)).
49
+ A name added to `StorageErrorCode` or `capabilityNames` is a minor release, so a `switch` over
50
+ either needs a default branch
51
+ ([spec 10](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#10-versions)).
52
+
53
+ ```ts
54
+ import { isStorageError } from "@stowage/core";
55
+
56
+ export function describeFailure(failure: unknown): string {
57
+ if (failure instanceof Error && failure.name === "AbortError") return "canceled";
58
+ if (!isStorageError(failure)) throw failure;
59
+
60
+ switch (failure.code) {
61
+ case "NotFound":
62
+ return "missing";
63
+ case "AccessDenied":
64
+ case "InvalidCredentials":
65
+ case "Expired":
66
+ return "refused";
67
+ default:
68
+ return failure.retryable ? "try again" : "failed";
69
+ }
70
+ }
71
+ ```
72
+
73
+ An adapter written outside this repository takes what two adapters here need on the wire from
74
+ this package rather than writing it again
75
+ ([spec 4.13](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#413-exports-for-adapter-authors)):
76
+
77
+ - `invalidKeyReason` checks a key against its rule of spec 4.8, as the first act of every
78
+ operation.
79
+ - `errorCodeForStatus` and `isTransientStatus` are the status mapping of spec 4.10, and
80
+ `withRetry` the retry loop around one request.
81
+ - `parseXml` reads the XML an answer document is written in, and rejects anything outside that
82
+ subset with `XmlSyntaxError`.
83
+ - `readEnvironment` reads one environment variable, and answers `""` where it is unset or the
84
+ runtime cannot read it.
85
+ - `isUserMetadataKey`, `encodeUserMetadataValue`, `decodeUserMetadataValue` and
86
+ `userMetadataByteLength` carry user metadata in headers, and measure the 2 KB of spec 4.3.
87
+ - `PresignedPut` is what `presignPut` resolves with on every adapter that declares
88
+ `presignedUrls`.
89
+
90
+ ```ts
91
+ import { invalidKeyReason, StorageError } from "@stowage/core";
92
+
93
+ export function requireWritableKey(bucket: string, key: string): void {
94
+ const reason = invalidKeyReason(key, "writable");
95
+
96
+ if (reason === undefined) return;
97
+
98
+ throw new StorageError({
99
+ code: "InvalidKey",
100
+ message: reason,
101
+ operation: "put",
102
+ bucket,
103
+ provider: "my-provider",
104
+ key,
105
+ attempts: 0,
106
+ });
107
+ }
108
+ ```
109
+
110
+ ## Specification
111
+
112
+ [`docs/spec.md` at `@stowage/core@0.2.0`](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#4-the-core-api-stowagecore)
113
+ is the contract: a caller may rely on what it states and on nothing else this package happens to
114
+ export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/CONTEXT.md)
115
+ and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/core@0.2.0/docs/adr)
116
+ are at the same tag.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,348 @@
1
+ //#region src/capabilities.d.ts
2
+ /**
3
+ * Every capability a storage can declare (spec 4.9). The list grows in minor releases, so a
4
+ * `switch` over it needs a default branch.
5
+ *
6
+ * - `keyBytesPreserved`: a key comes back byte for byte as it was written. Where not
7
+ * declared, it comes back Unicode-equivalent.
8
+ * - `presignedUrls`: the concrete type carries `presignGet` and `presignPut`. Where not
9
+ * declared, neither method exists on the type.
10
+ * - `rangeReads`: `get` honors `range`. Where not declared, a `range` is `Unsupported`.
11
+ * - `userMetadata`: `put` stores `userMetadata` whose keys are ASCII identifiers,
12
+ * `[A-Za-z_][A-Za-z0-9_]*`; `stat` and `get` return it, `copy` keeps it. Where not declared,
13
+ * a non-empty `userMetadata` is `Unsupported` and reads return `{}`.
14
+ * - `userMetadataTokenKeys`: beside `userMetadata`, a key may be any ASCII HTTP token, such as
15
+ * `content-hash`. Where not declared, a key outside identifiers is `Unsupported` naming it;
16
+ * without `userMetadata` too, the call is `Unsupported` naming that.
17
+ */
18
+ export declare const capabilityNames: readonly ["keyBytesPreserved", "presignedUrls", "rangeReads", "userMetadata", "userMetadataTokenKeys"];
19
+ /** One name out of {@link capabilityNames}. */
20
+ type CapabilityName = (typeof capabilityNames)[number];
21
+ //#endregion
22
+ //#region src/credentials.d.ts
23
+ /** What a resolver is told when the adapter asks for a credential again (spec 4.12). */
24
+ type ResolverOptions = {
25
+ forceRefresh: boolean;
26
+ };
27
+ /** A value, or a function that yields one. No core signature mentions it (ADR 0007). */
28
+ type Resolvable<T> = T | ((options?: ResolverOptions) => T | Promise<T>);
29
+ //#endregion
30
+ //#region src/environment.d.ts
31
+ /**
32
+ * ADR 0007: `process.env` is the one route through Node, Bun, Deno's compatibility layer
33
+ * and a Worker under `nodejs_compat`. A Worker without it has no `process` at all, and
34
+ * Deno without `--allow-env` throws `NotCapable` rather than answering `undefined`, so
35
+ * both leave the value empty. A name is read on its own, because enumerating
36
+ * `process.env` needs the unscoped permission in Deno.
37
+ */
38
+ export declare function readEnvironment(name: string): string;
39
+ //#endregion
40
+ //#region src/errors.d.ts
41
+ /**
42
+ * What went wrong, as the one field a caller branches on (spec 4.10). A code added here is a
43
+ * minor release, so a `switch` over it needs a default branch.
44
+ *
45
+ * - `NotFound`: no object under the key, or no bucket; `stat` cannot tell the two apart.
46
+ * - `AccessDenied`: the credential is valid and may not do this.
47
+ * - `InvalidCredentials`: the provider does not accept the credential, or a required
48
+ * credential field is empty or unknown.
49
+ * - `Expired`: the credential or session token has expired.
50
+ * - `InvalidRequest`: the provider or stowage refused the request for what it asked, such as
51
+ * metadata over the limit, an unsatisfiable range, a copy onto itself or a second read of a
52
+ * body.
53
+ * - `NetworkError`: the request received no response.
54
+ * - `ProviderError`: the provider answered with a failure stowage has no other name for;
55
+ * `providerCode` carries its string.
56
+ * - `InvalidKey`: the key violates the key rule, or a rule the adapter adds to it.
57
+ * - `InvalidOption`: an option or configuration value stowage refused, such as an unknown key,
58
+ * a value out of range or a cursor it did not produce.
59
+ * - `Unsupported`: the call needs a capability the storage does not declare; `capability`
60
+ * names it.
61
+ */
62
+ type StorageErrorCode = "NotFound" | "AccessDenied" | "InvalidCredentials" | "Expired" | "InvalidRequest" | "NetworkError" | "ProviderError" | "InvalidKey" | "InvalidOption" | "Unsupported";
63
+ interface StorageErrorFields {
64
+ readonly code: StorageErrorCode;
65
+ readonly message: string;
66
+ readonly operation: string;
67
+ readonly bucket: string;
68
+ readonly provider: string;
69
+ readonly attempts: number;
70
+ readonly key?: string;
71
+ readonly status?: number;
72
+ readonly providerCode?: string;
73
+ readonly requestId?: string;
74
+ readonly retryable?: boolean;
75
+ readonly capability?: CapabilityName;
76
+ readonly cause?: unknown;
77
+ }
78
+ /**
79
+ * A failure the rules of spec section 4 decide, stated without the adapter that raises it:
80
+ * the adapter adds what only it knows, its bucket, the operation and the attempts, and
81
+ * raises it through its own error factory (spec 4.13).
82
+ */
83
+ type Refusal = {
84
+ readonly code: "Unsupported";
85
+ readonly message: string;
86
+ readonly capability: CapabilityName;
87
+ } | {
88
+ readonly code: "InvalidRequest" | "InvalidOption";
89
+ readonly message: string;
90
+ };
91
+ export declare class StorageError extends Error {
92
+ readonly code: StorageErrorCode;
93
+ readonly operation: string;
94
+ readonly bucket: string;
95
+ readonly provider: string;
96
+ /**
97
+ * How often the failing step was attempted: `0` where stowage refused before the first
98
+ * attempt, `1` where a single attempt failed, more where the adapter repeated it.
99
+ */
100
+ readonly attempts: number;
101
+ /** The condition is transient. It says nothing about whether stowage sent the request again. */
102
+ readonly retryable: boolean;
103
+ readonly key?: string;
104
+ readonly status?: number;
105
+ readonly providerCode?: string;
106
+ readonly requestId?: string;
107
+ /** The capability an `Unsupported` failure needs, and set for no other code. */
108
+ readonly capability?: CapabilityName;
109
+ constructor(fields: StorageErrorFields);
110
+ }
111
+ export declare function isStorageError(value: unknown): value is StorageError;
112
+ //#endregion
113
+ //#region src/keys.d.ts
114
+ type KeyRule = "writable" | "addressable" | "prefix";
115
+ /** The reason a key violates the rule, or `undefined` where it holds. */
116
+ export declare function invalidKeyReason(key: string, rule: KeyRule): string | undefined;
117
+ //#endregion
118
+ //#region src/presigned-put.d.ts
119
+ /**
120
+ * What `presignPut` resolves with on every adapter that declares `presignedUrls`, so the code
121
+ * that uploads through it never names the provider (ADR 0022).
122
+ */
123
+ interface PresignedPut {
124
+ readonly url: string;
125
+ /** The headers the client sends beside the body. `Content-Length` is never among them. */
126
+ readonly headers: Readonly<Record<string, string>>;
127
+ }
128
+ //#endregion
129
+ //#region src/storage.d.ts
130
+ type PutBody = Uint8Array | string | ReadableStream<Uint8Array>;
131
+ interface OperationOptions {
132
+ signal?: AbortSignal;
133
+ }
134
+ interface PutOptions extends OperationOptions {
135
+ contentType?: string;
136
+ /**
137
+ * Stored where the storage declares `userMetadata`, and `Unsupported` elsewhere unless it is
138
+ * empty. Keys are non-empty ASCII HTTP tokens compared case-insensitively; values may hold
139
+ * any Unicode. Keys and values together hold at most 2 KB of encoded header bytes, and more
140
+ * is `InvalidRequest`. A key outside ASCII identifiers, such as `content-hash`, is
141
+ * `Unsupported` where the storage does not declare `userMetadataTokenKeys`.
142
+ */
143
+ userMetadata?: Record<string, string>;
144
+ }
145
+ /** Both ends inclusive; `end` absent means to the end of the object. */
146
+ interface ByteRange {
147
+ /**
148
+ * A non-negative integer no greater than `end`, else `InvalidOption`. At or beyond the
149
+ * object's size it is `InvalidRequest`.
150
+ */
151
+ start: number;
152
+ /** A non-negative integer, else `InvalidOption`. Beyond the object's size it is clipped. */
153
+ end?: number;
154
+ }
155
+ interface GetOptions extends OperationOptions {
156
+ range?: ByteRange;
157
+ }
158
+ interface ListOptions extends OperationOptions {
159
+ prefix?: string;
160
+ /** One or more characters; an empty string is `InvalidOption`. */
161
+ delimiter?: string;
162
+ /** 1 to 1000, and 1000 where absent. Outside that range it is `InvalidOption`. */
163
+ pageSize?: number;
164
+ /**
165
+ * The `cursor` of a page this storage produced, which a new listing in another process may
166
+ * continue from. Any other string is `InvalidOption`.
167
+ */
168
+ cursor?: string;
169
+ }
170
+ interface ObjectEntry {
171
+ readonly key: string;
172
+ readonly size: number;
173
+ readonly lastModified: Date;
174
+ readonly etag?: string;
175
+ }
176
+ interface ObjectStat extends ObjectEntry {
177
+ readonly contentType: string;
178
+ readonly userMetadata: Readonly<Record<string, string>>;
179
+ }
180
+ interface StoredObject {
181
+ readonly stat: ObjectStat;
182
+ stream(): ReadableStream<Uint8Array>;
183
+ bytes(): Promise<Uint8Array>;
184
+ text(): Promise<string>;
185
+ json<T = unknown>(): Promise<T>;
186
+ }
187
+ interface ListPage {
188
+ readonly objects: readonly ObjectEntry[];
189
+ readonly prefixes: readonly string[];
190
+ readonly cursor?: string;
191
+ }
192
+ interface ObjectListing extends AsyncIterable<ObjectEntry> {
193
+ page(): Promise<ListPage>;
194
+ }
195
+ interface DeleteReport {
196
+ readonly requested: number;
197
+ readonly failed: readonly StorageError[];
198
+ }
199
+ interface Storage {
200
+ readonly provider: string;
201
+ readonly bucket: string;
202
+ /** Every capability the storage implements, each once, fixed when it was constructed. */
203
+ readonly capabilities: readonly CapabilityName[];
204
+ put(key: string, body: PutBody, options?: PutOptions): Promise<ObjectStat>;
205
+ get(key: string, options?: GetOptions): Promise<StoredObject>;
206
+ stat(key: string, options?: OperationOptions): Promise<ObjectStat>;
207
+ exists(key: string, options?: OperationOptions): Promise<boolean>;
208
+ list(options?: ListOptions): ObjectListing;
209
+ /**
210
+ * Sends the keys in batches, one request per batch, and each adapter states its batch
211
+ * size. A per-key failure fills the report; a failure of a request as a whole rejects.
212
+ */
213
+ delete(...keys: readonly string[]): Promise<DeleteReport>;
214
+ deleteAll(prefix: string, options?: OperationOptions): Promise<DeleteReport>;
215
+ copy(from: string, to: string, options?: OperationOptions): Promise<ObjectStat>;
216
+ move(from: string, to: string, options?: OperationOptions): Promise<ObjectStat>;
217
+ }
218
+ //#endregion
219
+ //#region src/range.d.ts
220
+ /** Refuses bounds spec 4.3 does not allow, which is decided before the object is looked up. */
221
+ export declare function rangeBoundsRefusal(range: ByteRange | undefined): Refusal | undefined;
222
+ /**
223
+ * Refuses a range that starts at or beyond the object's `size` bytes, which spec 4.3 names a
224
+ * refusal of the request rather than of the option, because a provider answers it with `416`.
225
+ */
226
+ export declare function rangeStartRefusal(range: ByteRange, size: number, key: string): Refusal | undefined;
227
+ /** The last byte the range names, both ends inclusive, clipped to the object's `size` bytes. */
228
+ export declare function lastByteOf(range: ByteRange | undefined, size: number): number;
229
+ /**
230
+ * Whether the whole object is the body the range asks for, clipped as spec 4.3 clips it. A
231
+ * provider may answer a range with the whole object and `200`, which RFC 9110 allows, and
232
+ * that answer is the one asked for exactly where this holds. An empty object holds no byte a
233
+ * range could start at, so no range covers it.
234
+ */
235
+ export declare function rangeCoversWhole(range: ByteRange, size: number): boolean;
236
+ /** The `Range` field of RFC 9110, both ends inclusive as `ByteRange` is. */
237
+ export declare function rangeHeader(range: ByteRange): string;
238
+ /**
239
+ * The size of the whole object out of a `206`'s `Content-Range`, which is what spec 4.4 has a
240
+ * ranged `get` report rather than the length of the range.
241
+ */
242
+ export declare function wholeSizeOf(contentRange: string | null): number | undefined;
243
+ //#endregion
244
+ //#region src/retry.d.ts
245
+ interface RetryOptions {
246
+ readonly maxAttempts: number;
247
+ readonly signal?: AbortSignal;
248
+ }
249
+ /**
250
+ * Repeats `attempt` while it rejects with a `StorageError` whose `retryable` is `true`,
251
+ * up to `maxAttempts` times, waiting a random delay between zero and
252
+ * `min(5 s, 100 ms × 2^n)` before attempt `n + 1`. The error it finally rejects with
253
+ * carries the number of attempts made.
254
+ *
255
+ * Spec 4.13 has this be the one definition of the loop of spec 7.5, so that an adapter
256
+ * written outside this repository repeats on the same curve. `adapter-fs` and
257
+ * `adapter-memory` have no request to send again and do not call it.
258
+ */
259
+ export declare function withRetry<T>(attempt: () => Promise<T>, options: RetryOptions): Promise<T>;
260
+ //#endregion
261
+ //#region src/status.d.ts
262
+ /**
263
+ * The code the status decides on its own, or `undefined` for a status that decides
264
+ * nothing. It is the fallback of spec 4.10: an adapter maps a provider code first and
265
+ * reaches here where it recognizes none.
266
+ */
267
+ export declare function errorCodeForStatus(status: number): StorageErrorCode | undefined;
268
+ /** Whether the status names a condition that may be gone a moment later (spec 4.10). */
269
+ export declare function isTransientStatus(status: number): boolean;
270
+ //#endregion
271
+ //#region src/upload-stream.d.ts
272
+ interface StreamUploadOptions {
273
+ readonly partSize: number;
274
+ readonly concurrency: number;
275
+ /** The provider's limit on the parts of one upload. */
276
+ readonly maxParts: number;
277
+ /** What the `InvalidRequest` for a stream above `maxParts` is told against. */
278
+ readonly bucket: string;
279
+ readonly provider: string;
280
+ readonly key: string;
281
+ readonly signal?: AbortSignal;
282
+ }
283
+ interface StreamUpload<T> {
284
+ /** The stream ended within the first part: the bytes go as one request. */
285
+ whole(bytes: Uint8Array<ArrayBuffer>): Promise<T>;
286
+ /** The first part filled: the adapter starts, sends through `sendParts`, and commits. */
287
+ multipart(sendParts: SendParts): Promise<T>;
288
+ }
289
+ type SendParts = <R>(send: (index: number, bytes: Uint8Array<ArrayBuffer>, signal: AbortSignal) => Promise<R>) => Promise<{
290
+ readonly results: readonly R[];
291
+ readonly size: number;
292
+ }>;
293
+ /**
294
+ * Reads the stream into parts of `partSize` and hands a stream that ends within the first
295
+ * to `whole`, any other to `multipart`. The reader, its buffers and the cancellation of
296
+ * the source stay here, so the spec promises one call rather than the order of five
297
+ * (ADR 0030).
298
+ *
299
+ * The source is canceled wherever the upload settles before the stream ended, as spec 4.2
300
+ * has it for a `put` that rejects before reading its body to the end.
301
+ */
302
+ export declare function uploadStream<T>(stream: ReadableStream<Uint8Array>, options: StreamUploadOptions, upload: StreamUpload<T>): Promise<T>;
303
+ //#endregion
304
+ //#region src/user-metadata.d.ts
305
+ type UserMetadataKeyRule = "token" | "identifier";
306
+ export declare function isUserMetadataKey(name: string, rule: UserMetadataKeyRule): boolean;
307
+ /** The value as a header carries it: as written where it travels so, else as encoded words. */
308
+ export declare function encodeUserMetadataValue(value: string, options?: {
309
+ always?: boolean;
310
+ }): string;
311
+ /**
312
+ * What spec 4.3 bounds at 2 KB: every key and its value as `encodeUserMetadataValue` writes
313
+ * it, without `always`, so the bound does not depend on what an adapter encodes beyond the rule.
314
+ */
315
+ export declare function userMetadataByteLength(userMetadata: Readonly<Record<string, string>>): number;
316
+ type UserMetadataCheck = {
317
+ readonly held: Readonly<Record<string, string>>;
318
+ } | {
319
+ readonly refusal: Refusal;
320
+ };
321
+ /**
322
+ * The user metadata as a storage holds it, keys folded to lower case as a header field name
323
+ * is, or the first refusal of spec 4.3, read off what the storage declares.
324
+ */
325
+ export declare function checkUserMetadata(userMetadata: Record<string, string> | undefined, capabilities: readonly CapabilityName[]): UserMetadataCheck;
326
+ /** A header value read back, with every form of encoded word RFC 2047 allows decoded. */
327
+ export declare function decodeUserMetadataValue(value: string): string;
328
+ //#endregion
329
+ //#region src/xml.d.ts
330
+ interface XmlElement {
331
+ readonly name: string;
332
+ readonly attributes: Readonly<Record<string, string>>;
333
+ readonly children: readonly XmlElement[];
334
+ /** The text directly inside the element, its children's left out. */
335
+ readonly text: string;
336
+ }
337
+ /** A document outside the subset `parseXml` reads, reported rather than read around (ADR 0003). */
338
+ export declare class XmlSyntaxError extends Error {
339
+ override readonly name = "XmlSyntaxError";
340
+ }
341
+ /**
342
+ * The root element of an XML document of the subset the providers answer with: elements,
343
+ * attributes, text, comments, the five named entities and a numeric character reference to
344
+ * any Unicode scalar value but `U+0000`, under one optional declaration (spec 4.13).
345
+ */
346
+ export declare function parseXml(document: string): XmlElement;
347
+ //#endregion
348
+ export type { ByteRange, CapabilityName, DeleteReport, GetOptions, KeyRule, ListOptions, ListPage, ObjectEntry, ObjectListing, ObjectStat, OperationOptions, PresignedPut, PutBody, PutOptions, Refusal, Resolvable, ResolverOptions, RetryOptions, SendParts, Storage, StorageErrorCode, StorageErrorFields, StoredObject, StreamUpload, StreamUploadOptions, UserMetadataCheck, UserMetadataKeyRule, XmlElement };
package/dist/index.js ADDED
@@ -0,0 +1,832 @@
1
+ //#region src/capabilities.ts
2
+ /**
3
+ * Every capability a storage can declare (spec 4.9). The list grows in minor releases, so a
4
+ * `switch` over it needs a default branch.
5
+ *
6
+ * - `keyBytesPreserved`: a key comes back byte for byte as it was written. Where not
7
+ * declared, it comes back Unicode-equivalent.
8
+ * - `presignedUrls`: the concrete type carries `presignGet` and `presignPut`. Where not
9
+ * declared, neither method exists on the type.
10
+ * - `rangeReads`: `get` honors `range`. Where not declared, a `range` is `Unsupported`.
11
+ * - `userMetadata`: `put` stores `userMetadata` whose keys are ASCII identifiers,
12
+ * `[A-Za-z_][A-Za-z0-9_]*`; `stat` and `get` return it, `copy` keeps it. Where not declared,
13
+ * a non-empty `userMetadata` is `Unsupported` and reads return `{}`.
14
+ * - `userMetadataTokenKeys`: beside `userMetadata`, a key may be any ASCII HTTP token, such as
15
+ * `content-hash`. Where not declared, a key outside identifiers is `Unsupported` naming it;
16
+ * without `userMetadata` too, the call is `Unsupported` naming that.
17
+ */
18
+ const capabilityNames = [
19
+ "keyBytesPreserved",
20
+ "presignedUrls",
21
+ "rangeReads",
22
+ "userMetadata",
23
+ "userMetadataTokenKeys"
24
+ ];
25
+ //#endregion
26
+ //#region src/environment.ts
27
+ /**
28
+ * ADR 0007: `process.env` is the one route through Node, Bun, Deno's compatibility layer
29
+ * and a Worker under `nodejs_compat`. A Worker without it has no `process` at all, and
30
+ * Deno without `--allow-env` throws `NotCapable` rather than answering `undefined`, so
31
+ * both leave the value empty. A name is read on its own, because enumerating
32
+ * `process.env` needs the unscoped permission in Deno.
33
+ */
34
+ function readEnvironment(name) {
35
+ try {
36
+ if (typeof process === "undefined") return "";
37
+ return process?.env?.[name] ?? "";
38
+ } catch {
39
+ return "";
40
+ }
41
+ }
42
+ //#endregion
43
+ //#region src/errors.ts
44
+ const storageErrorBrand = Symbol.for("stowage.error");
45
+ var StorageError = class extends Error {
46
+ code;
47
+ operation;
48
+ bucket;
49
+ provider;
50
+ /**
51
+ * How often the failing step was attempted: `0` where stowage refused before the first
52
+ * attempt, `1` where a single attempt failed, more where the adapter repeated it.
53
+ */
54
+ attempts;
55
+ /** The condition is transient. It says nothing about whether stowage sent the request again. */
56
+ retryable;
57
+ key;
58
+ status;
59
+ providerCode;
60
+ requestId;
61
+ /** The capability an `Unsupported` failure needs, and set for no other code. */
62
+ capability;
63
+ constructor(fields) {
64
+ super(fields.message);
65
+ if (fields.code === "Unsupported" && fields.capability === void 0) throw new TypeError("An `Unsupported` storage error names the capability it needs");
66
+ this.name = "StorageError";
67
+ this.code = fields.code;
68
+ this.operation = fields.operation;
69
+ this.bucket = fields.bucket;
70
+ this.provider = fields.provider;
71
+ this.attempts = fields.attempts;
72
+ this.retryable = fields.retryable ?? false;
73
+ this.key = fields.key;
74
+ this.status = fields.status;
75
+ this.providerCode = fields.providerCode;
76
+ this.requestId = fields.requestId;
77
+ this.capability = fields.capability;
78
+ if (fields.cause !== void 0) this.cause = fields.cause;
79
+ Object.defineProperty(this, storageErrorBrand, { value: true });
80
+ }
81
+ };
82
+ function isStorageError(value) {
83
+ return typeof value === "object" && value !== null && storageErrorBrand in value;
84
+ }
85
+ /**
86
+ * The same failure counting the attempts a whole retry loop made rather than the one it
87
+ * was raised in. It lives beside the field list, because every field has to be named
88
+ * again here or it is dropped on the way through; the stack travels along, because it
89
+ * points at where the request failed and this is no other place it could have failed.
90
+ *
91
+ * Not published: spec 4.13 lists what an adapter calls, and `withRetry` is the one
92
+ * caller this has.
93
+ */
94
+ function withAttempts(failure, attempts) {
95
+ if (failure.attempts === attempts) return failure;
96
+ const counted = new StorageError({
97
+ code: failure.code,
98
+ message: failure.message,
99
+ operation: failure.operation,
100
+ bucket: failure.bucket,
101
+ provider: failure.provider,
102
+ attempts,
103
+ key: failure.key,
104
+ status: failure.status,
105
+ providerCode: failure.providerCode,
106
+ requestId: failure.requestId,
107
+ retryable: failure.retryable,
108
+ capability: failure.capability,
109
+ cause: failure.cause
110
+ });
111
+ counted.stack = failure.stack;
112
+ return counted;
113
+ }
114
+ //#endregion
115
+ //#region src/keys.ts
116
+ const writableKeyLimit = 1024;
117
+ const utf8$1 = new TextEncoder();
118
+ /** The reason a key violates the rule, or `undefined` where it holds. */
119
+ function invalidKeyReason(key, rule) {
120
+ if (key === "") return rule === "prefix" ? void 0 : "is empty";
121
+ const controlCharacter = firstControlCharacter(key);
122
+ if (controlCharacter !== void 0) return `holds the control character ${controlCharacter}`;
123
+ const loneSurrogate = firstLoneSurrogate(key);
124
+ if (loneSurrogate !== void 0) return `holds the lone surrogate ${loneSurrogate}`;
125
+ if (rule === "writable") {
126
+ if (key.includes("\\")) return "holds a backslash";
127
+ if (key.endsWith("/")) return "ends with a slash";
128
+ const bytes = utf8$1.encode(key).length;
129
+ if (bytes > writableKeyLimit) return `is ${bytes} UTF-8 bytes, above the limit of ${writableKeyLimit}`;
130
+ }
131
+ return invalidSegmentReason(key);
132
+ }
133
+ function firstControlCharacter(key) {
134
+ for (let index = 0; index < key.length; index += 1) {
135
+ const code = key.charCodeAt(index);
136
+ if (code <= 31 || code === 127) return codePointName(code);
137
+ }
138
+ }
139
+ function firstLoneSurrogate(key) {
140
+ if (key.isWellFormed()) return void 0;
141
+ for (const character of key) {
142
+ const code = character.codePointAt(0) ?? 0;
143
+ if (code >= 55296 && code <= 57343) return codePointName(code);
144
+ }
145
+ }
146
+ function codePointName(code) {
147
+ return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
148
+ }
149
+ function invalidSegmentReason(key) {
150
+ const segments = key.split("/");
151
+ for (const [index, segment] of segments.entries()) {
152
+ if (segment === "." || segment === "..") return `holds ${JSON.stringify(segment)} as a segment`;
153
+ if (segment !== "") continue;
154
+ if (index === 0) return "starts with a slash";
155
+ if (index < segments.length - 1) return "holds an empty segment";
156
+ }
157
+ }
158
+ //#endregion
159
+ //#region src/range.ts
160
+ const invalidBounds = {
161
+ code: "InvalidOption",
162
+ message: "The option `range` takes two whole numbers from zero up, `start` at most `end`"
163
+ };
164
+ /** Refuses bounds spec 4.3 does not allow, which is decided before the object is looked up. */
165
+ function rangeBoundsRefusal(range) {
166
+ if (range === void 0) return void 0;
167
+ const { start, end } = range;
168
+ if (!isOffset(start) || end !== void 0 && (!isOffset(end) || end < start)) return invalidBounds;
169
+ }
170
+ /**
171
+ * Refuses a range that starts at or beyond the object's `size` bytes, which spec 4.3 names a
172
+ * refusal of the request rather than of the option, because a provider answers it with `416`.
173
+ */
174
+ function rangeStartRefusal(range, size, key) {
175
+ if (range.start < size) return void 0;
176
+ return {
177
+ code: "InvalidRequest",
178
+ message: `The range starts beyond the ${size} bytes under the key ${JSON.stringify(key)}`
179
+ };
180
+ }
181
+ /** The last byte the range names, both ends inclusive, clipped to the object's `size` bytes. */
182
+ function lastByteOf(range, size) {
183
+ return Math.min(range?.end ?? size - 1, size - 1);
184
+ }
185
+ /**
186
+ * Whether the whole object is the body the range asks for, clipped as spec 4.3 clips it. A
187
+ * provider may answer a range with the whole object and `200`, which RFC 9110 allows, and
188
+ * that answer is the one asked for exactly where this holds. An empty object holds no byte a
189
+ * range could start at, so no range covers it.
190
+ */
191
+ function rangeCoversWhole(range, size) {
192
+ return range.start === 0 && size > 0 && lastByteOf(range, size) === size - 1;
193
+ }
194
+ /** The `Range` field of RFC 9110, both ends inclusive as `ByteRange` is. */
195
+ function rangeHeader(range) {
196
+ return `bytes=${range.start}-${range.end ?? ""}`;
197
+ }
198
+ const contentRangePattern = /^bytes (\d+)-(\d+)\/(\d+)$/u;
199
+ /**
200
+ * The size of the whole object out of a `206`'s `Content-Range`, which is what spec 4.4 has a
201
+ * ranged `get` report rather than the length of the range.
202
+ */
203
+ function wholeSizeOf(contentRange) {
204
+ const found = contentRangePattern.exec(contentRange?.trim() ?? "");
205
+ const size = Number(found?.[3]);
206
+ return Number.isSafeInteger(size) ? size : void 0;
207
+ }
208
+ function isOffset(value) {
209
+ return Number.isInteger(value) && value >= 0;
210
+ }
211
+ //#endregion
212
+ //#region src/retry.ts
213
+ const baseDelay = 100;
214
+ const maximumDelay = 5e3;
215
+ /**
216
+ * Repeats `attempt` while it rejects with a `StorageError` whose `retryable` is `true`,
217
+ * up to `maxAttempts` times, waiting a random delay between zero and
218
+ * `min(5 s, 100 ms × 2^n)` before attempt `n + 1`. The error it finally rejects with
219
+ * carries the number of attempts made.
220
+ *
221
+ * Spec 4.13 has this be the one definition of the loop of spec 7.5, so that an adapter
222
+ * written outside this repository repeats on the same curve. `adapter-fs` and
223
+ * `adapter-memory` have no request to send again and do not call it.
224
+ */
225
+ async function withRetry(attempt, options) {
226
+ let requestsSent = 0;
227
+ for (let attemptsMade = 1;; attemptsMade += 1) try {
228
+ return await attempt();
229
+ } catch (failure) {
230
+ requestsSent += costOf(failure);
231
+ if (!isStorageError(failure)) throw failure;
232
+ if (!failure.retryable || attemptsMade >= options.maxAttempts) throw withAttempts(failure, requestsSent);
233
+ await waitBefore(attemptsMade, options.signal);
234
+ }
235
+ }
236
+ function costOf(failure) {
237
+ return isStorageError(failure) ? failure.attempts : 1;
238
+ }
239
+ /** Full jitter (ADR 0013), interrupted by the signal that is the caller's whole budget. */
240
+ async function waitBefore(attemptsMade, signal) {
241
+ const milliseconds = Math.random() * Math.min(maximumDelay, baseDelay * 2 ** attemptsMade);
242
+ await new Promise((resolve, reject) => {
243
+ if (signal === void 0) {
244
+ setTimeout(resolve, milliseconds);
245
+ return;
246
+ }
247
+ signal.throwIfAborted();
248
+ const timer = setTimeout(() => {
249
+ signal.removeEventListener("abort", aborted);
250
+ resolve();
251
+ }, milliseconds);
252
+ const aborted = () => {
253
+ clearTimeout(timer);
254
+ reject(signal.reason);
255
+ };
256
+ signal.addEventListener("abort", aborted, { once: true });
257
+ });
258
+ }
259
+ //#endregion
260
+ //#region src/status.ts
261
+ /**
262
+ * The code the status decides on its own, or `undefined` for a status that decides
263
+ * nothing. It is the fallback of spec 4.10: an adapter maps a provider code first and
264
+ * reaches here where it recognizes none.
265
+ */
266
+ function errorCodeForStatus(status) {
267
+ if (status >= 200 && status <= 299) return void 0;
268
+ if (status === 401) return "InvalidCredentials";
269
+ if (status === 403) return "AccessDenied";
270
+ if (status === 404) return "NotFound";
271
+ return "ProviderError";
272
+ }
273
+ /** Whether the status names a condition that may be gone a moment later (spec 4.10). */
274
+ function isTransientStatus(status) {
275
+ return status === 408 || status === 429 || status >= 500 && status <= 599;
276
+ }
277
+ //#endregion
278
+ //#region src/part-reader.ts
279
+ /** Where a buffer starts before the stream has shown whether it fills a part. */
280
+ const initialCapacity = 65536;
281
+ /**
282
+ * A stream read into parts of one size (spec 4.13). A part is known to be the last only
283
+ * once the stream has ended behind it, so a full part looks one chunk ahead; that chunk
284
+ * is the stream's own and becomes the start of the next part.
285
+ *
286
+ * A part handed back through `recycle` lends its buffer to a later one. ADR 0016 bounds
287
+ * an upload at `partSize × concurrency` of buffers, and a fresh buffer per part would
288
+ * keep the settled ones alive until the collector noticed them.
289
+ *
290
+ * Until a part has filled, the buffer grows by doubling, because ADR 0009 has a body
291
+ * shorter than a part allocate only what it needs. Once one has, the stream is known to
292
+ * go as a multipart upload, and every buffer after it starts at the full part size.
293
+ */
294
+ var PartReader = class {
295
+ #reader;
296
+ #partSize;
297
+ #signal;
298
+ #cancelOnAbort = () => {
299
+ this.#reader.cancel(this.#signal?.reason).catch(() => {});
300
+ };
301
+ #free = [];
302
+ #ahead;
303
+ #filledOnce = false;
304
+ constructor(stream, partSize, signal) {
305
+ this.#reader = stream.getReader();
306
+ this.#partSize = partSize;
307
+ this.#signal = signal;
308
+ signal?.addEventListener("abort", this.#cancelOnAbort, { once: true });
309
+ }
310
+ async next() {
311
+ let bytes = this.#freshBuffer();
312
+ let filled = 0;
313
+ while (filled < this.#partSize) {
314
+ const chunk = this.#ahead ?? await this.#read();
315
+ this.#ahead = void 0;
316
+ if (chunk === void 0) return {
317
+ bytes: bytes.subarray(0, filled),
318
+ last: true
319
+ };
320
+ const taken = Math.min(chunk.byteLength, this.#partSize - filled);
321
+ if (filled + taken > bytes.byteLength) bytes = this.#grown(bytes, filled + taken);
322
+ bytes.set(chunk.subarray(0, taken), filled);
323
+ filled += taken;
324
+ if (taken < chunk.byteLength) this.#ahead = chunk.subarray(taken);
325
+ }
326
+ this.#filledOnce = true;
327
+ this.#ahead ??= await this.#read();
328
+ return {
329
+ bytes,
330
+ last: this.#ahead === void 0
331
+ };
332
+ }
333
+ /** The part's request settled, so nothing reads its bytes any more. */
334
+ recycle(part) {
335
+ if (part.bytes.buffer.byteLength === this.#partSize) this.#free.push(part.bytes.buffer);
336
+ }
337
+ /** Spec 4.2 leaves the stream canceled where `put` rejects before reading it to its end. */
338
+ async cancel(reason) {
339
+ await this.#reader.cancel(reason).catch(() => {});
340
+ }
341
+ release() {
342
+ this.#signal?.removeEventListener("abort", this.#cancelOnAbort);
343
+ this.#reader.releaseLock();
344
+ }
345
+ #freshBuffer() {
346
+ const pooled = this.#free.pop();
347
+ if (pooled !== void 0) return new Uint8Array(pooled);
348
+ return new Uint8Array(this.#filledOnce ? this.#partSize : Math.min(this.#partSize, initialCapacity));
349
+ }
350
+ #grown(bytes, needed) {
351
+ let capacity = bytes.byteLength;
352
+ while (capacity < needed) capacity *= 2;
353
+ const larger = new Uint8Array(Math.min(capacity, this.#partSize));
354
+ larger.set(bytes);
355
+ return larger;
356
+ }
357
+ async #read() {
358
+ for (;;) {
359
+ const { done, value } = await this.#reader.read();
360
+ this.#signal?.throwIfAborted();
361
+ if (done) return void 0;
362
+ if (value.byteLength > 0) return value;
363
+ }
364
+ }
365
+ };
366
+ //#endregion
367
+ //#region src/upload-stream.ts
368
+ /**
369
+ * Reads the stream into parts of `partSize` and hands a stream that ends within the first
370
+ * to `whole`, any other to `multipart`. The reader, its buffers and the cancellation of
371
+ * the source stay here, so the spec promises one call rather than the order of five
372
+ * (ADR 0030).
373
+ *
374
+ * The source is canceled wherever the upload settles before the stream ended, as spec 4.2
375
+ * has it for a `put` that rejects before reading its body to the end.
376
+ */
377
+ async function uploadStream(stream, options, upload) {
378
+ const parts = new PartReader(stream, options.partSize, options.signal);
379
+ const uploadSettled = new AbortController();
380
+ let readToEnd = false;
381
+ let cancelReason = /* @__PURE__ */ new Error("The upload settled before the stream ended");
382
+ try {
383
+ const first = await parts.next();
384
+ if (first.last) {
385
+ readToEnd = true;
386
+ return await upload.whole(first.bytes);
387
+ }
388
+ let called = false;
389
+ const sendParts = async (send) => {
390
+ uploadSettled.signal.throwIfAborted();
391
+ if (called) throw new Error("`sendParts` sends the parts of one upload once");
392
+ called = true;
393
+ const sent = await sendAll(parts, first, options, uploadSettled.signal, send);
394
+ readToEnd = true;
395
+ return sent;
396
+ };
397
+ return await upload.multipart(sendParts);
398
+ } catch (failure) {
399
+ cancelReason = failure;
400
+ throw failure;
401
+ } finally {
402
+ uploadSettled.abort(cancelReason);
403
+ if (!readToEnd) await parts.cancel(cancelReason);
404
+ parts.release();
405
+ }
406
+ }
407
+ /**
408
+ * `concurrency` parts in flight, and the next part read only once one of them settled,
409
+ * so the part buffers never outnumber the parts in flight (ADR 0016). The first failure
410
+ * stops the parts still in flight, and is what `sendParts` rejects with once they settled.
411
+ */
412
+ async function sendAll(parts, first, options, uploadSettled, send) {
413
+ const stop = new AbortController();
414
+ const partSignal = options.signal === void 0 ? stop.signal : AbortSignal.any([options.signal, stop.signal]);
415
+ const inFlight = /* @__PURE__ */ new Set();
416
+ const results = [];
417
+ let failure;
418
+ let size = 0;
419
+ const fail = (reason) => {
420
+ failure ??= { reason };
421
+ stop.abort();
422
+ parts.cancel(reason);
423
+ };
424
+ const sendPart = async (index, part) => {
425
+ try {
426
+ results[index] = await send(index, part.bytes, partSignal);
427
+ } catch (reason) {
428
+ fail(reason);
429
+ } finally {
430
+ parts.recycle(part);
431
+ }
432
+ };
433
+ const stopWithUpload = () => {
434
+ fail(uploadSettled.reason);
435
+ };
436
+ uploadSettled.addEventListener("abort", stopWithUpload, { once: true });
437
+ try {
438
+ for (let index = 0, part = first; !stop.signal.aborted; index += 1) {
439
+ if (index === options.maxParts - 1 && !part.last) throw tooManyParts(options);
440
+ const sending = sendPart(index, part);
441
+ inFlight.add(sending);
442
+ sending.finally(() => inFlight.delete(sending));
443
+ size += part.bytes.byteLength;
444
+ if (part.last) break;
445
+ while (inFlight.size >= options.concurrency) await Promise.race(inFlight);
446
+ part = await parts.next();
447
+ }
448
+ } catch (reason) {
449
+ fail(reason);
450
+ }
451
+ await Promise.all(inFlight);
452
+ uploadSettled.removeEventListener("abort", stopWithUpload);
453
+ if (failure !== void 0) {
454
+ await parts.cancel(failure.reason);
455
+ throw failure.reason;
456
+ }
457
+ return {
458
+ results,
459
+ size
460
+ };
461
+ }
462
+ /**
463
+ * Known once the last part the provider takes is full and the stream goes on. ADR 0016
464
+ * fixes the part size before the first part, so the way past it is a larger one.
465
+ */
466
+ function tooManyParts(options) {
467
+ return new StorageError({
468
+ code: "InvalidRequest",
469
+ message: `The stream needs more than ${options.maxParts} parts of the configured \`partSize\` of ${options.partSize} bytes; a larger \`multipart.partSize\` carries it`,
470
+ operation: "put",
471
+ bucket: options.bucket,
472
+ provider: options.provider,
473
+ key: options.key,
474
+ attempts: 0
475
+ });
476
+ }
477
+ //#endregion
478
+ //#region src/user-metadata.ts
479
+ const keyPatterns = {
480
+ /** The characters RFC 9110 allows in a field name, which is what a header carries. */
481
+ token: /^[!#$%&'*+.^_`|~\dA-Za-z-]+$/u,
482
+ /** An ASCII identifier, which Azure requires of a metadata name (ADR 0020). */
483
+ identifier: /^[A-Za-z_][A-Za-z\d_]*$/u
484
+ };
485
+ function isUserMetadataKey(name, rule) {
486
+ return keyPatterns[rule].test(name);
487
+ }
488
+ /**
489
+ * What survives a header field as it stands: printable ASCII with no space at either end,
490
+ * which HTTP trims, and no `=?`, which the reader would take for the start of an encoded
491
+ * word.
492
+ */
493
+ const travelsAsWritten = /^(?:[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?)?$/u;
494
+ const encodedWordStart = "=?UTF-8?B?";
495
+ const encodedWordEnd = "?=";
496
+ /**
497
+ * RFC 2047 bounds an encoded word at 75 characters; 45 bytes are 60 characters of base64,
498
+ * which leaves room for the 12 the word's frame costs.
499
+ */
500
+ const bytesPerEncodedWord = 45;
501
+ const utf8 = new TextEncoder();
502
+ /** The value as a header carries it: as written where it travels so, else as encoded words. */
503
+ function encodeUserMetadataValue(value, options) {
504
+ if (value === "") return value;
505
+ if (options?.always !== true && travelsAsWritten.test(value) && !value.includes("=?")) return value;
506
+ return utf8PiecesOf(value).map((piece) => `${encodedWordStart}${btoa(String.fromCharCode(...piece))}${encodedWordEnd}`).join(" ");
507
+ }
508
+ /**
509
+ * What spec 4.3 bounds at 2 KB: every key and its value as `encodeUserMetadataValue` writes
510
+ * it, without `always`, so the bound does not depend on what an adapter encodes beyond the rule.
511
+ */
512
+ function userMetadataByteLength(userMetadata) {
513
+ let bytes = 0;
514
+ for (const [name, value] of Object.entries(userMetadata)) bytes += utf8.encode(name).length + encodeUserMetadataValue(value).length;
515
+ return bytes;
516
+ }
517
+ /** Spec 4.3 bounds the set at 2 KB of the header bytes it costs once it is encoded. */
518
+ const headerByteLimit = 2048;
519
+ const noUserMetadata = { held: Object.freeze(Object.create(null)) };
520
+ /**
521
+ * The user metadata as a storage holds it, keys folded to lower case as a header field name
522
+ * is, or the first refusal of spec 4.3, read off what the storage declares.
523
+ */
524
+ function checkUserMetadata(userMetadata, capabilities) {
525
+ const entries = Object.entries(userMetadata ?? {});
526
+ if (entries.length === 0) return noUserMetadata;
527
+ if (!capabilities.includes("userMetadata")) return unsupported("userMetadata", "This storage holds no user metadata");
528
+ const held = Object.create(null);
529
+ for (const [name, value] of entries) {
530
+ if (!isUserMetadataKey(name, "token")) return refused(`The user metadata key ${JSON.stringify(name)} is no ASCII HTTP token`);
531
+ const folded = name.toLowerCase();
532
+ if (folded in held) return refused(`The user metadata key ${JSON.stringify(folded)} is given more than once`);
533
+ held[folded] = value;
534
+ }
535
+ const malformed = Object.entries(held).find(([, value]) => !value.isWellFormed());
536
+ if (malformed !== void 0) return refused(`The user metadata value of ${JSON.stringify(malformed[0])} holds a lone surrogate, which has no UTF-8 form`);
537
+ const headerBytes = userMetadataByteLength(held);
538
+ if (headerBytes > headerByteLimit) return refused(`The user metadata is ${headerBytes} encoded header bytes, above the limit of ${headerByteLimit}`);
539
+ const beyondIdentifiers = entries.find(([name]) => !isUserMetadataKey(name, "identifier"));
540
+ if (beyondIdentifiers !== void 0 && !capabilities.includes("userMetadataTokenKeys")) return unsupported("userMetadataTokenKeys", `This storage holds no user metadata key beyond identifiers, such as ${JSON.stringify(beyondIdentifiers[0])}`);
541
+ return { held: Object.freeze(held) };
542
+ }
543
+ function refused(message) {
544
+ return { refusal: {
545
+ code: "InvalidRequest",
546
+ message
547
+ } };
548
+ }
549
+ function unsupported(capability, message) {
550
+ return { refusal: {
551
+ code: "Unsupported",
552
+ message,
553
+ capability
554
+ } };
555
+ }
556
+ /** The UTF-8 bytes in pieces of at most one encoded word, each ending on a character. */
557
+ function utf8PiecesOf(value) {
558
+ const pieces = [];
559
+ let pending = [];
560
+ for (const character of value) {
561
+ const bytes = utf8.encode(character);
562
+ if (pending.length + bytes.length > bytesPerEncodedWord) {
563
+ pieces.push(Uint8Array.from(pending));
564
+ pending = [];
565
+ }
566
+ pending.push(...bytes);
567
+ }
568
+ pieces.push(Uint8Array.from(pending));
569
+ return pieces;
570
+ }
571
+ const encodedWord = /=\?([^?\s]+)\?([BbQq])\?([^?\s]*)\?=/gu;
572
+ const whitespaceOnly = /^[ \t]+$/u;
573
+ const hexPair = /^[\dA-Fa-f]{2}$/u;
574
+ /** A header value read back, with every form of encoded word RFC 2047 allows decoded. */
575
+ function decodeUserMetadataValue(value) {
576
+ let decoded = "";
577
+ let last = 0;
578
+ let previousWasEncoded = false;
579
+ for (const match of value.matchAll(encodedWord)) {
580
+ const between = value.slice(last, match.index);
581
+ const word = decodeWord(match[1] ?? "", match[2] ?? "", match[3] ?? "");
582
+ if (!(previousWasEncoded && word !== void 0 && whitespaceOnly.test(between))) decoded += between;
583
+ decoded += word ?? match[0];
584
+ previousWasEncoded = word !== void 0;
585
+ last = match.index + match[0].length;
586
+ }
587
+ return decoded + value.slice(last);
588
+ }
589
+ /** The text of one encoded word, or `undefined` where it names what cannot be read. */
590
+ function decodeWord(charset, encoding, text) {
591
+ const bytes = encoding.toUpperCase() === "B" ? base64Bytes(text) : quotedBytes(text);
592
+ if (bytes === void 0) return void 0;
593
+ try {
594
+ return new TextDecoder(charset, { fatal: true }).decode(bytes);
595
+ } catch {
596
+ return;
597
+ }
598
+ }
599
+ function base64Bytes(text) {
600
+ try {
601
+ return Uint8Array.from(atob(text), (character) => character.charCodeAt(0));
602
+ } catch {
603
+ return;
604
+ }
605
+ }
606
+ /** The `Q` encoding: `_` is a space and `=XX` one byte in hex. */
607
+ function quotedBytes(text) {
608
+ const bytes = [];
609
+ for (let index = 0; index < text.length; index += 1) {
610
+ const character = text[index];
611
+ if (character === "_") bytes.push(32);
612
+ else if (character === "=") {
613
+ const pair = text.slice(index + 1, index + 3);
614
+ if (!hexPair.test(pair)) return void 0;
615
+ bytes.push(Number.parseInt(pair, 16));
616
+ index += 2;
617
+ } else bytes.push(text.charCodeAt(index));
618
+ }
619
+ return Uint8Array.from(bytes);
620
+ }
621
+ //#endregion
622
+ //#region src/xml.ts
623
+ /** A document outside the subset `parseXml` reads, reported rather than read around (ADR 0003). */
624
+ var XmlSyntaxError = class extends Error {
625
+ name = "XmlSyntaxError";
626
+ };
627
+ /**
628
+ * The root element of an XML document of the subset the providers answer with: elements,
629
+ * attributes, text, comments, the five named entities and a numeric character reference to
630
+ * any Unicode scalar value but `U+0000`, under one optional declaration (spec 4.13).
631
+ */
632
+ function parseXml(document) {
633
+ const scanner = new Scanner(document);
634
+ scanner.skipDeclaration();
635
+ scanner.skipMisc();
636
+ const root = scanner.readElement();
637
+ scanner.skipMisc();
638
+ scanner.requireEnd();
639
+ return root;
640
+ }
641
+ const namePattern = /[:A-Z_a-z\u00C0-\uFFFF][:A-Z_a-z\u00C0-\uFFFF.\d-]*/uy;
642
+ const whitespacePattern = /[ \t\r\n]*/y;
643
+ const entityPattern = /&(?:#x([\da-fA-F]+)|#(\d+)|([A-Za-z]\w*));/uy;
644
+ /**
645
+ * What a character reference may name: any Unicode scalar value but `U+0000`. That is
646
+ * wider than `Char` of XML 1.0 on purpose, because S3 writes a key character XML cannot
647
+ * carry, such as `U+FFFE`, as a reference, and the key has to read back byte for byte
648
+ * (ADR 0027).
649
+ */
650
+ function isReferableCharacter(codePoint) {
651
+ return codePoint >= 1 && codePoint <= 55295 || codePoint >= 57344 && codePoint <= 1114111;
652
+ }
653
+ /** The five entities XML defines without a DTD, which is every one a document here has. */
654
+ const predefinedEntities = /* @__PURE__ */ new Map([
655
+ ["amp", "&"],
656
+ ["lt", "<"],
657
+ ["gt", ">"],
658
+ ["quot", "\""],
659
+ ["apos", "'"]
660
+ ]);
661
+ var Scanner = class {
662
+ #document;
663
+ #position = 0;
664
+ constructor(document) {
665
+ this.#document = document;
666
+ }
667
+ skipDeclaration() {
668
+ if (!this.#startsWith("<?xml")) return;
669
+ this.#position = this.#positionPast("?>", "The XML declaration is never closed");
670
+ }
671
+ /** Whitespace and comments, which are all that may stand beside the root element. */
672
+ skipMisc() {
673
+ for (;;) {
674
+ this.#skipWhitespace();
675
+ if (this.#startsWith("<!DOCTYPE")) throw this.#error("A DTD is refused");
676
+ if (!this.#startsWith("<!--")) return;
677
+ this.#skipComment();
678
+ }
679
+ }
680
+ requireEnd() {
681
+ if (this.#position < this.#document.length) throw this.#error("Something follows the root element");
682
+ }
683
+ readElement() {
684
+ this.#expect("<");
685
+ const name = this.#readName();
686
+ const attributes = Object.create(null);
687
+ if (this.#readStartTagEnd(attributes) === "empty") return {
688
+ name,
689
+ attributes,
690
+ children: [],
691
+ text: ""
692
+ };
693
+ const children = [];
694
+ let text = "";
695
+ for (;;) {
696
+ if (this.#position >= this.#document.length) throw this.#error(`The element <${name}> is never closed`);
697
+ if (this.#startsWith("</")) {
698
+ this.#position += 2;
699
+ this.#closeElement(name);
700
+ return {
701
+ name,
702
+ attributes,
703
+ children,
704
+ text
705
+ };
706
+ }
707
+ if (this.#startsWith("<!--")) this.#skipComment();
708
+ else if (this.#startsWith("<![CDATA[")) throw this.#error("A CDATA section is refused");
709
+ else if (this.#startsWith("<")) children.push(this.readElement());
710
+ else text += this.#readText();
711
+ }
712
+ }
713
+ #closeElement(name) {
714
+ const closing = this.#readName();
715
+ if (closing !== name) throw this.#error(`The element <${name}> is closed by </${closing}>`);
716
+ this.#skipWhitespace();
717
+ this.#expect(">");
718
+ }
719
+ #readStartTagEnd(attributes) {
720
+ for (;;) {
721
+ const before = this.#position;
722
+ this.#skipWhitespace();
723
+ if (this.#startsWith("/>")) {
724
+ this.#position += 2;
725
+ return "empty";
726
+ }
727
+ if (this.#startsWith(">")) {
728
+ this.#position += 1;
729
+ return "open";
730
+ }
731
+ if (this.#position === before) throw this.#error("An attribute follows no whitespace");
732
+ const attribute = this.#readName();
733
+ if (attribute in attributes) throw this.#error(`The attribute ${attribute} is given twice`);
734
+ this.#skipWhitespace();
735
+ this.#expect("=");
736
+ this.#skipWhitespace();
737
+ attributes[attribute] = this.#readAttributeValue();
738
+ }
739
+ }
740
+ /**
741
+ * XML 1.0, section 3.3.3: without a DTD every attribute is CDATA, so each whitespace
742
+ * character written into the value reads as a space, and a line break as one space. A
743
+ * reference to one stays the character it names.
744
+ */
745
+ #readAttributeValue() {
746
+ const quote = this.#document[this.#position];
747
+ if (quote !== "\"" && quote !== "'") throw this.#error("An attribute value is not quoted");
748
+ this.#position += 1;
749
+ let value = "";
750
+ for (;;) {
751
+ const character = this.#document[this.#position];
752
+ if (character === void 0) throw this.#error("An attribute value is never closed");
753
+ if (character === quote) {
754
+ this.#position += 1;
755
+ return value;
756
+ }
757
+ if (character === "<") throw this.#error("A `<` stands inside an attribute value");
758
+ if (character === "&") {
759
+ value += this.#readEntity();
760
+ continue;
761
+ }
762
+ this.#position += this.#startsWith("\r\n") ? 2 : 1;
763
+ value += character === " " || character === "\n" || character === "\r" ? " " : character;
764
+ }
765
+ }
766
+ /** Text up to the next markup, every `&` in it the start of an entity it decodes. */
767
+ #readText() {
768
+ const markup = this.#document.indexOf("<", this.#position);
769
+ const end = markup === -1 ? this.#document.length : markup;
770
+ let text = "";
771
+ while (this.#position < end) {
772
+ const ampersand = this.#document.indexOf("&", this.#position);
773
+ if (ampersand === -1 || ampersand >= end) {
774
+ text += this.#document.slice(this.#position, end);
775
+ this.#position = end;
776
+ break;
777
+ }
778
+ text += this.#document.slice(this.#position, ampersand);
779
+ this.#position = ampersand;
780
+ text += this.#readEntity();
781
+ }
782
+ return text;
783
+ }
784
+ #readEntity() {
785
+ entityPattern.lastIndex = this.#position;
786
+ const found = entityPattern.exec(this.#document);
787
+ if (found === null) throw this.#error("An `&` starts no entity");
788
+ const [entity, hex, decimal, name] = found;
789
+ if (name !== void 0) {
790
+ const character = predefinedEntities.get(name);
791
+ if (character === void 0) throw this.#error(`The entity ${entity} is not defined`);
792
+ this.#position = entityPattern.lastIndex;
793
+ return character;
794
+ }
795
+ const codePoint = hex === void 0 ? Number(decimal) : Number.parseInt(hex, 16);
796
+ if (!isReferableCharacter(codePoint)) throw this.#error(`The reference ${entity} names U+0000, a surrogate or no Unicode scalar value at all`);
797
+ this.#position = entityPattern.lastIndex;
798
+ return String.fromCodePoint(codePoint);
799
+ }
800
+ #skipComment() {
801
+ this.#position = this.#positionPast("-->", "A comment is never closed", 4);
802
+ }
803
+ #readName() {
804
+ namePattern.lastIndex = this.#position;
805
+ const found = namePattern.exec(this.#document);
806
+ if (found === null) throw this.#error("A name is missing where one belongs");
807
+ this.#position = namePattern.lastIndex;
808
+ return found[0];
809
+ }
810
+ #skipWhitespace() {
811
+ whitespacePattern.lastIndex = this.#position;
812
+ whitespacePattern.exec(this.#document);
813
+ this.#position = whitespacePattern.lastIndex;
814
+ }
815
+ #expect(literal) {
816
+ if (!this.#startsWith(literal)) throw this.#error(`\`${literal}\` is missing`);
817
+ this.#position += literal.length;
818
+ }
819
+ #positionPast(terminator, unterminated, offset = 0) {
820
+ const end = this.#document.indexOf(terminator, this.#position + offset);
821
+ if (end === -1) throw this.#error(unterminated);
822
+ return end + terminator.length;
823
+ }
824
+ #startsWith(literal) {
825
+ return this.#document.startsWith(literal, this.#position);
826
+ }
827
+ #error(message) {
828
+ return new XmlSyntaxError(`${message} (at character ${this.#position})`);
829
+ }
830
+ };
831
+ //#endregion
832
+ export { StorageError, XmlSyntaxError, capabilityNames, checkUserMetadata, decodeUserMetadataValue, encodeUserMetadataValue, errorCodeForStatus, invalidKeyReason, isStorageError, isTransientStatus, isUserMetadataKey, lastByteOf, parseXml, rangeBoundsRefusal, rangeCoversWhole, rangeHeader, rangeStartRefusal, readEnvironment, uploadStream, userMetadataByteLength, wholeSizeOf, withRetry };
package/package.json CHANGED
@@ -1 +1,38 @@
1
- {"name":"@stowage/core","version":"0.0.0","description":"Placeholder that reserves the name for trusted publishing; the first release is 0.1.0.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/stowage-js/stowage.git","directory":"packages/core"}}
1
+ {
2
+ "name": "@stowage/core",
3
+ "version": "0.2.0",
4
+ "description": "One typed API for object storage: the types every stowage adapter implements, StorageError, and the utilities an adapter calls.",
5
+ "keywords": [
6
+ "adapter",
7
+ "object-storage",
8
+ "s3",
9
+ "storage",
10
+ "stowage",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/stowage-js/stowage/tree/main/packages/core#readme",
14
+ "bugs": "https://github.com/stowage-js/stowage/issues",
15
+ "license": "MIT",
16
+ "author": "Alexander Kaufmann",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/stowage-js/stowage.git",
20
+ "directory": "packages/core"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": "./dist/index.js",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "provenance": true
34
+ },
35
+ "engines": {
36
+ "node": ">=24"
37
+ }
38
+ }