@btravstack/storage 0.38.0 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/dist/index.cjs +63 -39
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +46 -24
- package/dist/{storage-EA671z1R.cjs → options-BhML_i5p.cjs} +35 -0
- package/dist/{storage-BIKstQ72.mjs → options-DYPXkWb9.mjs} +19 -2
- package/dist/s3.cjs +72 -43
- package/dist/s3.d.cts +1 -1
- package/dist/s3.d.mts +1 -1
- package/dist/s3.mjs +67 -38
- package/dist/{storage-BavPXZpA.d.cts → storage-CfN3KqWm.d.cts} +40 -8
- package/dist/{storage-BavPXZpA.d.mts → storage-CfN3KqWm.d.mts} +40 -8
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ export const invoices = InvoiceProvider(Invoices)({
|
|
|
67
67
|
// is swallowed because an observer already recorded it and
|
|
68
68
|
// counted it one layer down.
|
|
69
69
|
.recoverErrCases((matcher) =>
|
|
70
|
-
matcher.with(P.tag("StorageUnavailable"), () => undefined),
|
|
70
|
+
matcher.with(P.tag("StorageUnavailable"), P.tag("InvalidStorageOptions"), P.tag("ChecksumMismatch"), () => undefined),
|
|
71
71
|
),
|
|
72
72
|
// Serving bytes through your own process is the anti-pattern this arm
|
|
73
73
|
// exists to avoid: the caller fetches the object directly, for a minute.
|
|
@@ -106,13 +106,16 @@ export const InvoicesApp = Module("InvoicesApp")({
|
|
|
106
106
|
|
|
107
107
|
## Options
|
|
108
108
|
|
|
109
|
-
| Option | Where | What it is
|
|
110
|
-
| ----------------------------------------------------------- | ---------------------------------- |
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
109
|
+
| Option | Where | What it is |
|
|
110
|
+
| ----------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------- |
|
|
111
|
+
| `sha256` / `metadata` | `put` / `putIfAbsent` | verify the write digest and atomically describe the object |
|
|
112
|
+
| `checksum` / `metadata` | `copy` | compute SHA-256 server side and replace the description |
|
|
113
|
+
| `kmsKeyId` | writes, copy and `presignedUpload` | select per-object SSE-KMS; upload clients send both signed encryption headers |
|
|
114
|
+
| `adapter` | `storage({ adapter })` | the adapter module providing `StorageBackend` — required |
|
|
115
|
+
| `STORAGE_S3_ENDPOINT` | environment, read by `s3Storage()` | the store's endpoint — required |
|
|
116
|
+
| `STORAGE_S3_BUCKET` | environment | the bucket — required |
|
|
117
|
+
| `STORAGE_S3_ACCESS_KEY_ID` / `STORAGE_S3_SECRET_ACCESS_KEY` | environment | the credentials — required |
|
|
118
|
+
| `STORAGE_S3_REGION` | environment | default `us-east-1` |
|
|
116
119
|
|
|
117
120
|
The full table — defaults, semantics and the reasoning — lives on
|
|
118
121
|
[the reference page](https://btravstack.github.io/btravstack/reference/storage),
|
|
@@ -121,7 +124,7 @@ which is this list's one detailed home.
|
|
|
121
124
|
## What it decides, and what it does not
|
|
122
125
|
|
|
123
126
|
**It decides** that a missing object is an ordinary answer rather than a fault
|
|
124
|
-
(counted `not_found`,
|
|
127
|
+
(counted `not_found`, without a fault line), that a held key under `putIfAbsent` is an ordinary answer (`ObjectExists`, counted `exists`), that `delete` is idempotent, that `copy` and `head` never move the bytes through the process, that writes can verify SHA-256 and publish user metadata atomically, that copies replace metadata and can compute SHA-256 server side, that presigned uploads can require a per-object KMS key, that
|
|
125
128
|
presigning asks the store nothing — and that an adapter which cannot presign
|
|
126
129
|
says so instead of minting a URL that would fail only in production.
|
|
127
130
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_options = require("./options-BhML_i5p.cjs");
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
let _btravstack_di = require("@btravstack/di");
|
|
5
5
|
let unthrown = require("unthrown");
|
|
@@ -19,34 +19,38 @@ let _btravstack_core = require("@btravstack/core");
|
|
|
19
19
|
*/
|
|
20
20
|
const memoryStorageBackend = () => {
|
|
21
21
|
const objects = /* @__PURE__ */ new Map();
|
|
22
|
+
const verify = (key, bytes, options) => require_options.validateOptions(key, options).flatMap(() => options.sha256 !== void 0 && (0, node_crypto.createHash)("sha256").update(bytes).digest("hex") !== options.sha256 ? (0, unthrown.Err)(new require_options.ChecksumMismatch({ key })) : (0, unthrown.Ok)());
|
|
22
23
|
return {
|
|
23
|
-
put: (key, bytes, options) => {
|
|
24
|
+
put: (key, bytes, options) => verify(key, bytes, options).map(() => {
|
|
24
25
|
objects.set(key, {
|
|
25
|
-
bytes:
|
|
26
|
-
contentType: options.contentType
|
|
26
|
+
bytes: Uint8Array.from(bytes),
|
|
27
|
+
contentType: options.contentType,
|
|
28
|
+
metadata: { ...options.metadata }
|
|
27
29
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (objects.has(key)) return (0, unthrown.ErrAsync)(new require_storage.ObjectExists({ key }));
|
|
30
|
+
}).toAsync(),
|
|
31
|
+
putIfAbsent: (key, bytes, options) => verify(key, bytes, options).flatMap(() => {
|
|
32
|
+
if (objects.has(key)) return (0, unthrown.Err)(new require_options.ObjectExists({ key }));
|
|
32
33
|
objects.set(key, {
|
|
33
|
-
bytes:
|
|
34
|
-
contentType: options.contentType
|
|
34
|
+
bytes: Uint8Array.from(bytes),
|
|
35
|
+
contentType: options.contentType,
|
|
36
|
+
metadata: { ...options.metadata }
|
|
35
37
|
});
|
|
36
|
-
return (0, unthrown.
|
|
37
|
-
},
|
|
38
|
-
copy: (from, to, options) => {
|
|
38
|
+
return (0, unthrown.Ok)();
|
|
39
|
+
}).toAsync(),
|
|
40
|
+
copy: (from, to, options) => require_options.validateOptions(to, options).flatMap(() => {
|
|
39
41
|
const object = objects.get(from);
|
|
40
|
-
if (object === void 0) return (0, unthrown.
|
|
42
|
+
if (object === void 0) return (0, unthrown.Err)(new require_options.ObjectNotFound({ key: from }));
|
|
41
43
|
objects.set(to, {
|
|
42
44
|
bytes: object.bytes.slice(),
|
|
43
|
-
contentType: options.contentType
|
|
45
|
+
contentType: options.contentType,
|
|
46
|
+
metadata: { ...options.metadata }
|
|
44
47
|
});
|
|
45
|
-
return (0, unthrown.
|
|
46
|
-
},
|
|
48
|
+
return (0, unthrown.Ok)();
|
|
49
|
+
}).toAsync(),
|
|
47
50
|
head: (key) => {
|
|
48
51
|
const object = objects.get(key);
|
|
49
|
-
return object === void 0 ? (0, unthrown.ErrAsync)(new
|
|
52
|
+
return object === void 0 ? (0, unthrown.ErrAsync)(new require_options.ObjectNotFound({ key })) : (0, unthrown.OkAsync)({
|
|
53
|
+
metadata: { ...object.metadata },
|
|
50
54
|
size: object.bytes.byteLength,
|
|
51
55
|
contentType: object.contentType,
|
|
52
56
|
sha256: (0, node_crypto.createHash)("sha256").update(object.bytes).digest("hex")
|
|
@@ -54,8 +58,8 @@ const memoryStorageBackend = () => {
|
|
|
54
58
|
},
|
|
55
59
|
get: (key) => {
|
|
56
60
|
const object = objects.get(key);
|
|
57
|
-
return object === void 0 ? (0, unthrown.ErrAsync)(new
|
|
58
|
-
|
|
61
|
+
return object === void 0 ? (0, unthrown.ErrAsync)(new require_options.ObjectNotFound({ key })) : (0, unthrown.OkAsync)({
|
|
62
|
+
contentType: object.contentType,
|
|
59
63
|
bytes: object.bytes.slice()
|
|
60
64
|
});
|
|
61
65
|
},
|
|
@@ -63,19 +67,19 @@ const memoryStorageBackend = () => {
|
|
|
63
67
|
objects.delete(key);
|
|
64
68
|
return (0, unthrown.OkAsync)();
|
|
65
69
|
},
|
|
66
|
-
presignedUrl: (key) => (0, unthrown.ErrAsync)(new
|
|
67
|
-
presignedUpload: (key) => (0, unthrown.ErrAsync)(new
|
|
70
|
+
presignedUrl: (key) => (0, unthrown.ErrAsync)(new require_options.PresignNotSupported({ key })),
|
|
71
|
+
presignedUpload: (key) => (0, unthrown.ErrAsync)(new require_options.PresignNotSupported({ key }))
|
|
68
72
|
};
|
|
69
73
|
};
|
|
70
74
|
/** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
|
|
71
|
-
const memoryStorageProvider = () => (0, _btravstack_di.Provider)(
|
|
75
|
+
const memoryStorageProvider = () => (0, _btravstack_di.Provider)(require_options.StorageBackend)({
|
|
72
76
|
inject: {},
|
|
73
77
|
sync: () => memoryStorageBackend()
|
|
74
78
|
});
|
|
75
79
|
/** The adapter as a module, which is the shape `storage({ adapter })` takes. */
|
|
76
80
|
const memoryStorage = () => (0, _btravstack_di.Module)("MemoryStorage")({
|
|
77
81
|
provides: [memoryStorageProvider()],
|
|
78
|
-
exports: [
|
|
82
|
+
exports: [require_options.StorageBackend]
|
|
79
83
|
});
|
|
80
84
|
//#endregion
|
|
81
85
|
//#region src/instrument.ts
|
|
@@ -83,8 +87,8 @@ const memoryStorage = () => (0, _btravstack_di.Module)("MemoryStorage")({
|
|
|
83
87
|
* **A missing object settles `ok` with `result: "not_found"`, not `error`.**
|
|
84
88
|
* Asking for something that is not there is an ordinary answer, and a dashboard
|
|
85
89
|
* that treats it as a fault teaches its readers to ignore the fault line.
|
|
86
|
-
* `StorageUnavailable` is what pages somebody
|
|
87
|
-
*
|
|
90
|
+
* `StorageUnavailable` is what pages somebody; invalid options and checksum
|
|
91
|
+
* mismatches also settle as errors. The two ordinary answers keep their own words in
|
|
88
92
|
* `reason`, so an operator is not left hunting an object sitting where they put
|
|
89
93
|
* it.
|
|
90
94
|
*/
|
|
@@ -126,10 +130,28 @@ const through = (observers) => (operation, key, call) => (0, _btravstack_core.ob
|
|
|
126
130
|
const instrument = (backend, observers) => {
|
|
127
131
|
const observe = through(observers);
|
|
128
132
|
return {
|
|
129
|
-
put: (key, bytes, options) =>
|
|
130
|
-
|
|
133
|
+
put: (key, bytes, options) => {
|
|
134
|
+
const snapshot = {
|
|
135
|
+
...options,
|
|
136
|
+
metadata: { ...options.metadata }
|
|
137
|
+
};
|
|
138
|
+
return observe("put", key, () => backend.put(key, bytes, snapshot));
|
|
139
|
+
},
|
|
140
|
+
putIfAbsent: (key, bytes, options) => {
|
|
141
|
+
const snapshot = {
|
|
142
|
+
...options,
|
|
143
|
+
metadata: { ...options.metadata }
|
|
144
|
+
};
|
|
145
|
+
return observe("put_if_absent", key, () => backend.putIfAbsent(key, bytes, snapshot));
|
|
146
|
+
},
|
|
131
147
|
get: (key) => observe("get", key, () => backend.get(key)),
|
|
132
|
-
copy: (from, to, options) =>
|
|
148
|
+
copy: (from, to, options) => {
|
|
149
|
+
const snapshot = {
|
|
150
|
+
...options,
|
|
151
|
+
metadata: { ...options.metadata }
|
|
152
|
+
};
|
|
153
|
+
return observe("copy", from, () => backend.copy(from, to, snapshot));
|
|
154
|
+
},
|
|
133
155
|
head: (key) => observe("head", key, () => backend.head(key)),
|
|
134
156
|
delete: (key) => observe("delete", key, () => backend.delete(key)),
|
|
135
157
|
presignedUrl: (key, options) => observe("presigned_url", key, () => backend.presignedUrl(key, options)),
|
|
@@ -156,7 +178,7 @@ const instrument = (backend, observers) => {
|
|
|
156
178
|
*/
|
|
157
179
|
const storage = ({ adapter }) => {
|
|
158
180
|
const healthCheck = _btravstack_di.Provider.member(_btravstack_core.HealthChecks)({
|
|
159
|
-
inject: { backend:
|
|
181
|
+
inject: { backend: require_options.StorageBackend },
|
|
160
182
|
sync: ({ backend }) => ({
|
|
161
183
|
name: "storage",
|
|
162
184
|
check: () => backend.get("btravstack:health").map(() => void 0).flatMapErrCases((matcher) => matcher.with(unthrown.P.tag("ObjectNotFound"), () => (0, unthrown.Ok)(void 0)).with(unthrown.P.tag("StorageUnavailable"), ({ reason }) => (0, unthrown.Err)(new _btravstack_core.HealthCheckFailed({ reason: `storage unavailable (${reason})` }))))
|
|
@@ -166,25 +188,27 @@ const storage = ({ adapter }) => {
|
|
|
166
188
|
imports: [adapter],
|
|
167
189
|
provides: [
|
|
168
190
|
_btravstack_core.noObserverMember,
|
|
169
|
-
(0, _btravstack_di.Provider)(
|
|
191
|
+
(0, _btravstack_di.Provider)(require_options.Storage)({
|
|
170
192
|
inject: {
|
|
171
|
-
backend:
|
|
193
|
+
backend: require_options.StorageBackend,
|
|
172
194
|
observers: _btravstack_core.Observers
|
|
173
195
|
},
|
|
174
196
|
sync: ({ backend, observers }) => instrument(backend, observers)
|
|
175
197
|
}),
|
|
176
198
|
healthCheck
|
|
177
199
|
],
|
|
178
|
-
exports: [
|
|
200
|
+
exports: [require_options.Storage, _btravstack_core.HealthChecks]
|
|
179
201
|
});
|
|
180
202
|
};
|
|
181
203
|
//#endregion
|
|
182
|
-
exports.
|
|
183
|
-
exports.
|
|
184
|
-
exports.
|
|
185
|
-
exports.
|
|
186
|
-
exports.
|
|
187
|
-
exports.
|
|
204
|
+
exports.ChecksumMismatch = require_options.ChecksumMismatch;
|
|
205
|
+
exports.InvalidStorageOptions = require_options.InvalidStorageOptions;
|
|
206
|
+
exports.ObjectExists = require_options.ObjectExists;
|
|
207
|
+
exports.ObjectNotFound = require_options.ObjectNotFound;
|
|
208
|
+
exports.PresignNotSupported = require_options.PresignNotSupported;
|
|
209
|
+
exports.Storage = require_options.Storage;
|
|
210
|
+
exports.StorageBackend = require_options.StorageBackend;
|
|
211
|
+
exports.StorageUnavailable = require_options.StorageUnavailable;
|
|
188
212
|
exports.memoryStorage = memoryStorage;
|
|
189
213
|
exports.memoryStorageBackend = memoryStorageBackend;
|
|
190
214
|
exports.memoryStorageProvider = memoryStorageProvider;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as ObjectHead, c as Storage, d as StorageUnavailable, f as StoredObject, i as ObjectExists, l as StorageBackend, n as CopyOptions, o as ObjectNotFound, p as WriteOptions, r as InvalidStorageOptions, s as PresignNotSupported, t as ChecksumMismatch, u as StorageService } from "./storage-CfN3KqWm.cjs";
|
|
2
2
|
import { Module, Provider } from "@btravstack/di";
|
|
3
3
|
import { HealthChecks } from "@btravstack/core";
|
|
4
4
|
//#region src/memory.d.ts
|
|
@@ -49,4 +49,4 @@ type StorageOptions<E, N> = {
|
|
|
49
49
|
*/
|
|
50
50
|
export declare const storage: <E, N>({ adapter }: StorageOptions<E, N>) => Module<Storage | HealthChecks, E, N>;
|
|
51
51
|
//#endregion
|
|
52
|
-
export { ObjectExists, type ObjectHead, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, type WriteOptions };
|
|
52
|
+
export { ChecksumMismatch, type CopyOptions, InvalidStorageOptions, ObjectExists, type ObjectHead, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, type WriteOptions };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as ObjectHead, c as Storage, d as StorageUnavailable, f as StoredObject, i as ObjectExists, l as StorageBackend, n as CopyOptions, o as ObjectNotFound, p as WriteOptions, r as InvalidStorageOptions, s as PresignNotSupported, t as ChecksumMismatch, u as StorageService } from "./storage-CfN3KqWm.mjs";
|
|
2
2
|
import { Module, Provider } from "@btravstack/di";
|
|
3
3
|
import { HealthChecks } from "@btravstack/core";
|
|
4
4
|
//#region src/memory.d.ts
|
|
@@ -49,4 +49,4 @@ type StorageOptions<E, N> = {
|
|
|
49
49
|
*/
|
|
50
50
|
export declare const storage: <E, N>({ adapter }: StorageOptions<E, N>) => Module<Storage | HealthChecks, E, N>;
|
|
51
51
|
//#endregion
|
|
52
|
-
export { ObjectExists, type ObjectHead, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, type WriteOptions };
|
|
52
|
+
export { ChecksumMismatch, type CopyOptions, InvalidStorageOptions, ObjectExists, type ObjectHead, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, type WriteOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as StorageBackend, i as
|
|
1
|
+
import { a as ObjectNotFound, c as StorageBackend, i as ObjectExists, l as StorageUnavailable, n as ChecksumMismatch, o as PresignNotSupported, r as InvalidStorageOptions, s as Storage, t as validateOptions } from "./options-DYPXkWb9.mjs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { Module, Provider } from "@btravstack/di";
|
|
4
4
|
import { Err, ErrAsync, Ok, OkAsync, P } from "unthrown";
|
|
@@ -18,34 +18,38 @@ import { HealthCheckFailed, HealthChecks, Observers, noObserverMember, observed
|
|
|
18
18
|
*/
|
|
19
19
|
const memoryStorageBackend = () => {
|
|
20
20
|
const objects = /* @__PURE__ */ new Map();
|
|
21
|
+
const verify = (key, bytes, options) => validateOptions(key, options).flatMap(() => options.sha256 !== void 0 && createHash("sha256").update(bytes).digest("hex") !== options.sha256 ? Err(new ChecksumMismatch({ key })) : Ok());
|
|
21
22
|
return {
|
|
22
|
-
put: (key, bytes, options) => {
|
|
23
|
+
put: (key, bytes, options) => verify(key, bytes, options).map(() => {
|
|
23
24
|
objects.set(key, {
|
|
24
|
-
bytes:
|
|
25
|
-
contentType: options.contentType
|
|
25
|
+
bytes: Uint8Array.from(bytes),
|
|
26
|
+
contentType: options.contentType,
|
|
27
|
+
metadata: { ...options.metadata }
|
|
26
28
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (objects.has(key)) return ErrAsync(new ObjectExists({ key }));
|
|
29
|
+
}).toAsync(),
|
|
30
|
+
putIfAbsent: (key, bytes, options) => verify(key, bytes, options).flatMap(() => {
|
|
31
|
+
if (objects.has(key)) return Err(new ObjectExists({ key }));
|
|
31
32
|
objects.set(key, {
|
|
32
|
-
bytes:
|
|
33
|
-
contentType: options.contentType
|
|
33
|
+
bytes: Uint8Array.from(bytes),
|
|
34
|
+
contentType: options.contentType,
|
|
35
|
+
metadata: { ...options.metadata }
|
|
34
36
|
});
|
|
35
|
-
return
|
|
36
|
-
},
|
|
37
|
-
copy: (from, to, options) => {
|
|
37
|
+
return Ok();
|
|
38
|
+
}).toAsync(),
|
|
39
|
+
copy: (from, to, options) => validateOptions(to, options).flatMap(() => {
|
|
38
40
|
const object = objects.get(from);
|
|
39
|
-
if (object === void 0) return
|
|
41
|
+
if (object === void 0) return Err(new ObjectNotFound({ key: from }));
|
|
40
42
|
objects.set(to, {
|
|
41
43
|
bytes: object.bytes.slice(),
|
|
42
|
-
contentType: options.contentType
|
|
44
|
+
contentType: options.contentType,
|
|
45
|
+
metadata: { ...options.metadata }
|
|
43
46
|
});
|
|
44
|
-
return
|
|
45
|
-
},
|
|
47
|
+
return Ok();
|
|
48
|
+
}).toAsync(),
|
|
46
49
|
head: (key) => {
|
|
47
50
|
const object = objects.get(key);
|
|
48
51
|
return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync({
|
|
52
|
+
metadata: { ...object.metadata },
|
|
49
53
|
size: object.bytes.byteLength,
|
|
50
54
|
contentType: object.contentType,
|
|
51
55
|
sha256: createHash("sha256").update(object.bytes).digest("hex")
|
|
@@ -54,7 +58,7 @@ const memoryStorageBackend = () => {
|
|
|
54
58
|
get: (key) => {
|
|
55
59
|
const object = objects.get(key);
|
|
56
60
|
return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync({
|
|
57
|
-
|
|
61
|
+
contentType: object.contentType,
|
|
58
62
|
bytes: object.bytes.slice()
|
|
59
63
|
});
|
|
60
64
|
},
|
|
@@ -82,8 +86,8 @@ const memoryStorage = () => Module("MemoryStorage")({
|
|
|
82
86
|
* **A missing object settles `ok` with `result: "not_found"`, not `error`.**
|
|
83
87
|
* Asking for something that is not there is an ordinary answer, and a dashboard
|
|
84
88
|
* that treats it as a fault teaches its readers to ignore the fault line.
|
|
85
|
-
* `StorageUnavailable` is what pages somebody
|
|
86
|
-
*
|
|
89
|
+
* `StorageUnavailable` is what pages somebody; invalid options and checksum
|
|
90
|
+
* mismatches also settle as errors. The two ordinary answers keep their own words in
|
|
87
91
|
* `reason`, so an operator is not left hunting an object sitting where they put
|
|
88
92
|
* it.
|
|
89
93
|
*/
|
|
@@ -125,10 +129,28 @@ const through = (observers) => (operation, key, call) => observed(observers, {
|
|
|
125
129
|
const instrument = (backend, observers) => {
|
|
126
130
|
const observe = through(observers);
|
|
127
131
|
return {
|
|
128
|
-
put: (key, bytes, options) =>
|
|
129
|
-
|
|
132
|
+
put: (key, bytes, options) => {
|
|
133
|
+
const snapshot = {
|
|
134
|
+
...options,
|
|
135
|
+
metadata: { ...options.metadata }
|
|
136
|
+
};
|
|
137
|
+
return observe("put", key, () => backend.put(key, bytes, snapshot));
|
|
138
|
+
},
|
|
139
|
+
putIfAbsent: (key, bytes, options) => {
|
|
140
|
+
const snapshot = {
|
|
141
|
+
...options,
|
|
142
|
+
metadata: { ...options.metadata }
|
|
143
|
+
};
|
|
144
|
+
return observe("put_if_absent", key, () => backend.putIfAbsent(key, bytes, snapshot));
|
|
145
|
+
},
|
|
130
146
|
get: (key) => observe("get", key, () => backend.get(key)),
|
|
131
|
-
copy: (from, to, options) =>
|
|
147
|
+
copy: (from, to, options) => {
|
|
148
|
+
const snapshot = {
|
|
149
|
+
...options,
|
|
150
|
+
metadata: { ...options.metadata }
|
|
151
|
+
};
|
|
152
|
+
return observe("copy", from, () => backend.copy(from, to, snapshot));
|
|
153
|
+
},
|
|
132
154
|
head: (key) => observe("head", key, () => backend.head(key)),
|
|
133
155
|
delete: (key) => observe("delete", key, () => backend.delete(key)),
|
|
134
156
|
presignedUrl: (key, options) => observe("presigned_url", key, () => backend.presignedUrl(key, options)),
|
|
@@ -178,4 +200,4 @@ const storage = ({ adapter }) => {
|
|
|
178
200
|
});
|
|
179
201
|
};
|
|
180
202
|
//#endregion
|
|
181
|
-
export { ObjectExists, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, StorageUnavailable, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
|
|
203
|
+
export { ChecksumMismatch, InvalidStorageOptions, ObjectExists, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, StorageUnavailable, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
|
|
@@ -5,6 +5,10 @@ let unthrown = require("unthrown");
|
|
|
5
5
|
var ObjectNotFound = class extends (0, unthrown.TaggedError)("ObjectNotFound") {};
|
|
6
6
|
/** An object already holds the key. Only `putIfAbsent` answers it. */
|
|
7
7
|
var ObjectExists = class extends (0, unthrown.TaggedError)("ObjectExists") {};
|
|
8
|
+
/** Invalid options are refused before any write or signature is attempted. */
|
|
9
|
+
var InvalidStorageOptions = class extends (0, unthrown.TaggedError)("InvalidStorageOptions") {};
|
|
10
|
+
/** The supplied SHA-256 did not match the body; no object was written. */
|
|
11
|
+
var ChecksumMismatch = class extends (0, unthrown.TaggedError)("ChecksumMismatch") {};
|
|
8
12
|
/** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
9
13
|
var StorageUnavailable = class extends (0, unthrown.TaggedError)("StorageUnavailable") {};
|
|
10
14
|
/** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
|
|
@@ -24,6 +28,31 @@ var Storage = class extends (0, _btravstack_di.Port)("Storage") {};
|
|
|
24
28
|
/** The port every adapter provides, and the one an application never depends on. */
|
|
25
29
|
var StorageBackend = class extends (0, _btravstack_di.Port)("StorageBackend") {};
|
|
26
30
|
//#endregion
|
|
31
|
+
//#region src/options.ts
|
|
32
|
+
const validateOptions = (key, options) => {
|
|
33
|
+
if (options.sha256 !== void 0 && !/^[a-f0-9]{64}$/.test(options.sha256)) return (0, unthrown.Err)(new InvalidStorageOptions({
|
|
34
|
+
key,
|
|
35
|
+
reason: "invalid_sha256"
|
|
36
|
+
}));
|
|
37
|
+
if (Object.keys(options.metadata ?? {}).some((name) => !/^[!#$%&'*+\-.^_`|~0-9a-z]+$/.test(name))) return (0, unthrown.Err)(new InvalidStorageOptions({
|
|
38
|
+
key,
|
|
39
|
+
reason: "invalid_metadata_key"
|
|
40
|
+
}));
|
|
41
|
+
return (0, unthrown.Ok)();
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
Object.defineProperty(exports, "ChecksumMismatch", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
get: function() {
|
|
47
|
+
return ChecksumMismatch;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(exports, "InvalidStorageOptions", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
get: function() {
|
|
53
|
+
return InvalidStorageOptions;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
27
56
|
Object.defineProperty(exports, "ObjectExists", {
|
|
28
57
|
enumerable: true,
|
|
29
58
|
get: function() {
|
|
@@ -60,3 +89,9 @@ Object.defineProperty(exports, "StorageUnavailable", {
|
|
|
60
89
|
return StorageUnavailable;
|
|
61
90
|
}
|
|
62
91
|
});
|
|
92
|
+
Object.defineProperty(exports, "validateOptions", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
get: function() {
|
|
95
|
+
return validateOptions;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Port } from "@btravstack/di";
|
|
2
|
-
import { TaggedError } from "unthrown";
|
|
2
|
+
import { Err, Ok, TaggedError } from "unthrown";
|
|
3
3
|
//#region src/storage.ts
|
|
4
4
|
/** The key is not there. Only `get`, `head` and `copy` (its source) answer it — see `delete` and `presignedUrl` below. */
|
|
5
5
|
var ObjectNotFound = class extends TaggedError("ObjectNotFound") {};
|
|
6
6
|
/** An object already holds the key. Only `putIfAbsent` answers it. */
|
|
7
7
|
var ObjectExists = class extends TaggedError("ObjectExists") {};
|
|
8
|
+
/** Invalid options are refused before any write or signature is attempted. */
|
|
9
|
+
var InvalidStorageOptions = class extends TaggedError("InvalidStorageOptions") {};
|
|
10
|
+
/** The supplied SHA-256 did not match the body; no object was written. */
|
|
11
|
+
var ChecksumMismatch = class extends TaggedError("ChecksumMismatch") {};
|
|
8
12
|
/** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
9
13
|
var StorageUnavailable = class extends TaggedError("StorageUnavailable") {};
|
|
10
14
|
/** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
|
|
@@ -24,4 +28,17 @@ var Storage = class extends Port("Storage") {};
|
|
|
24
28
|
/** The port every adapter provides, and the one an application never depends on. */
|
|
25
29
|
var StorageBackend = class extends Port("StorageBackend") {};
|
|
26
30
|
//#endregion
|
|
27
|
-
|
|
31
|
+
//#region src/options.ts
|
|
32
|
+
const validateOptions = (key, options) => {
|
|
33
|
+
if (options.sha256 !== void 0 && !/^[a-f0-9]{64}$/.test(options.sha256)) return Err(new InvalidStorageOptions({
|
|
34
|
+
key,
|
|
35
|
+
reason: "invalid_sha256"
|
|
36
|
+
}));
|
|
37
|
+
if (Object.keys(options.metadata ?? {}).some((name) => !/^[!#$%&'*+\-.^_`|~0-9a-z]+$/.test(name))) return Err(new InvalidStorageOptions({
|
|
38
|
+
key,
|
|
39
|
+
reason: "invalid_metadata_key"
|
|
40
|
+
}));
|
|
41
|
+
return Ok();
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { ObjectNotFound as a, StorageBackend as c, ObjectExists as i, StorageUnavailable as l, ChecksumMismatch as n, PresignNotSupported as o, InvalidStorageOptions as r, Storage as s, validateOptions as t };
|
package/dist/s3.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_options = require("./options-BhML_i5p.cjs");
|
|
3
3
|
let _btravstack_di = require("@btravstack/di");
|
|
4
4
|
let unthrown = require("unthrown");
|
|
5
5
|
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
@@ -30,13 +30,14 @@ const s3Schema = _btravstack_config.Config.object({
|
|
|
30
30
|
* close — so the client rides the graph and the scope closing destroys it.
|
|
31
31
|
*/
|
|
32
32
|
var S3Connection = class extends (0, _btravstack_di.Port)("S3Connection") {};
|
|
33
|
-
const unavailable = (operation, key, cause) => new
|
|
33
|
+
const unavailable = (operation, key, cause) => new require_options.StorageUnavailable({
|
|
34
34
|
operation,
|
|
35
35
|
key,
|
|
36
36
|
reason: cause instanceof Error ? cause.message : String(cause)
|
|
37
37
|
});
|
|
38
38
|
/** S3's answer to `If-None-Match: *` on a key that is held: HTTP 412. */
|
|
39
39
|
const isPreconditionFailed = (cause) => cause?.$metadata?.httpStatusCode === 412;
|
|
40
|
+
const isChecksumMismatch = (cause) => cause instanceof Error && (cause.name === "BadDigest" || cause.name === "InvalidDigest");
|
|
40
41
|
/** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
|
|
41
42
|
const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
|
|
42
43
|
/** SSE-KMS parameters when a key is named; none otherwise, so the bucket's default applies. */
|
|
@@ -53,47 +54,66 @@ const base64ToHex = (base64) => Buffer.from(base64, "base64").toString("hex");
|
|
|
53
54
|
* fiction layered over it.
|
|
54
55
|
*/
|
|
55
56
|
const s3StorageBackend = (client, bucket) => ({
|
|
56
|
-
put: (key, bytes, options) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
57
|
+
put: (key, bytes, options) => {
|
|
58
|
+
const validated = require_options.validateOptions(key, options);
|
|
59
|
+
if (!validated.isOk()) return validated.toAsync();
|
|
60
|
+
return (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.PutObjectCommand({
|
|
61
|
+
Bucket: bucket,
|
|
62
|
+
Key: key,
|
|
63
|
+
Body: bytes,
|
|
64
|
+
ContentType: options.contentType,
|
|
65
|
+
Metadata: { ...options.metadata },
|
|
66
|
+
...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) },
|
|
67
|
+
...encryption(options)
|
|
68
|
+
})), (cause) => isChecksumMismatch(cause) ? new require_options.ChecksumMismatch({ key }) : unavailable("put", key, cause)).map(() => void 0);
|
|
69
|
+
},
|
|
70
|
+
putIfAbsent: (key, bytes, options) => {
|
|
71
|
+
const validated = require_options.validateOptions(key, options);
|
|
72
|
+
if (!validated.isOk()) return validated.toAsync();
|
|
73
|
+
return (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.PutObjectCommand({
|
|
74
|
+
Bucket: bucket,
|
|
75
|
+
Key: key,
|
|
76
|
+
Body: bytes,
|
|
77
|
+
ContentType: options.contentType,
|
|
78
|
+
IfNoneMatch: "*",
|
|
79
|
+
Metadata: { ...options.metadata },
|
|
80
|
+
...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) },
|
|
81
|
+
...encryption(options)
|
|
82
|
+
})), (cause) => isChecksumMismatch(cause) ? new require_options.ChecksumMismatch({ key }) : isPreconditionFailed(cause) ? new require_options.ObjectExists({ key }) : unavailable("putIfAbsent", key, cause)).map(() => void 0);
|
|
83
|
+
},
|
|
71
84
|
get: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.GetObjectCommand({
|
|
72
85
|
Bucket: bucket,
|
|
73
86
|
Key: key
|
|
74
87
|
})).then(async (response) => ({
|
|
75
88
|
bytes: await response.Body.transformToByteArray(),
|
|
76
89
|
contentType: response.ContentType ?? "application/octet-stream"
|
|
77
|
-
})), (cause) => isMissing(cause) ? new
|
|
78
|
-
copy: (from, to, options) =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
})), (cause) => isMissing(cause) ? new require_options.ObjectNotFound({ key }) : unavailable("get", key, cause)),
|
|
91
|
+
copy: (from, to, options) => {
|
|
92
|
+
const validated = require_options.validateOptions(to, options);
|
|
93
|
+
if (!validated.isOk()) return validated.toAsync();
|
|
94
|
+
return (0, unthrown.fromPromise)((async () => client.send(new _aws_sdk_client_s3.CopyObjectCommand({
|
|
95
|
+
Bucket: bucket,
|
|
96
|
+
Key: to,
|
|
97
|
+
CopySource: `${bucket}/${from.split("/").map(encodeURIComponent).join("/")}`,
|
|
98
|
+
MetadataDirective: "REPLACE",
|
|
99
|
+
TaggingDirective: "REPLACE",
|
|
100
|
+
AnnotationDirective: "EXCLUDE",
|
|
101
|
+
ContentType: options.contentType,
|
|
102
|
+
Metadata: { ...options.metadata },
|
|
103
|
+
...options.checksum === void 0 ? {} : { ChecksumAlgorithm: "SHA256" },
|
|
104
|
+
...encryption(options)
|
|
105
|
+
})))(), (cause) => isMissing(cause) ? new require_options.ObjectNotFound({ key: from }) : unavailable("copy", from, cause)).map(() => void 0);
|
|
106
|
+
},
|
|
88
107
|
head: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.HeadObjectCommand({
|
|
89
108
|
Bucket: bucket,
|
|
90
109
|
Key: key,
|
|
91
110
|
ChecksumMode: "ENABLED"
|
|
92
111
|
})).then((response) => ({
|
|
112
|
+
metadata: { ...response.Metadata },
|
|
93
113
|
size: response.ContentLength ?? 0,
|
|
94
114
|
contentType: response.ContentType ?? "application/octet-stream",
|
|
95
|
-
...response.ChecksumSHA256 === void 0 || response.ChecksumType === "COMPOSITE" ? {} : { sha256: base64ToHex(response.ChecksumSHA256) }
|
|
96
|
-
})), (cause) => isMissing(cause) ? new
|
|
115
|
+
...response.ChecksumSHA256 === void 0 || response.ChecksumType === "COMPOSITE" || !/^[A-Za-z0-9+/]{43}=$/.test(response.ChecksumSHA256) ? {} : { sha256: base64ToHex(response.ChecksumSHA256) }
|
|
116
|
+
})), (cause) => isMissing(cause) ? new require_options.ObjectNotFound({ key }) : unavailable("head", key, cause)),
|
|
97
117
|
delete: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
98
118
|
Bucket: bucket,
|
|
99
119
|
Key: key
|
|
@@ -102,17 +122,26 @@ const s3StorageBackend = (client, bucket) => ({
|
|
|
102
122
|
Bucket: bucket,
|
|
103
123
|
Key: key
|
|
104
124
|
}), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause)),
|
|
105
|
-
presignedUpload: (key, options) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
presignedUpload: (key, options) => {
|
|
126
|
+
const command = new _aws_sdk_client_s3.PutObjectCommand({
|
|
127
|
+
Bucket: bucket,
|
|
128
|
+
Key: key,
|
|
129
|
+
ContentType: options.contentType,
|
|
130
|
+
ContentLength: options.contentLength,
|
|
131
|
+
...encryption(options),
|
|
132
|
+
...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) }
|
|
133
|
+
});
|
|
134
|
+
const expiresIn = Math.ceil(options.ttlMs / 1e3);
|
|
135
|
+
return require_options.validateOptions(key, options).toAsync().flatMap(() => (0, unthrown.fromPromise)((0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, command, {
|
|
136
|
+
expiresIn,
|
|
137
|
+
signableHeaders: /* @__PURE__ */ new Set(["content-type"]),
|
|
138
|
+
unhoistableHeaders: /* @__PURE__ */ new Set([
|
|
139
|
+
"x-amz-checksum-sha256",
|
|
140
|
+
"x-amz-server-side-encryption",
|
|
141
|
+
"x-amz-server-side-encryption-aws-kms-key-id"
|
|
142
|
+
])
|
|
143
|
+
}), (cause) => unavailable("presignedUpload", key, cause)));
|
|
144
|
+
}
|
|
116
145
|
});
|
|
117
146
|
/**
|
|
118
147
|
* The S3 adapter: one client, built with the scope and destroyed with it, and
|
|
@@ -142,12 +171,12 @@ const s3Storage = () => (0, _btravstack_di.Module)("S3Storage")({
|
|
|
142
171
|
client.destroy();
|
|
143
172
|
}
|
|
144
173
|
}),
|
|
145
|
-
(0, _btravstack_di.Provider)(
|
|
174
|
+
(0, _btravstack_di.Provider)(require_options.StorageBackend)({
|
|
146
175
|
inject: { connection: S3Connection },
|
|
147
176
|
sync: ({ connection }) => s3StorageBackend(connection.client, connection.bucket)
|
|
148
177
|
})
|
|
149
178
|
],
|
|
150
|
-
exports: [
|
|
179
|
+
exports: [require_options.StorageBackend]
|
|
151
180
|
});
|
|
152
181
|
//#endregion
|
|
153
182
|
exports.s3Storage = s3Storage;
|
package/dist/s3.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as StorageBackend, u as StorageService } from "./storage-CfN3KqWm.cjs";
|
|
2
2
|
import { Module, Scope } from "@btravstack/di";
|
|
3
3
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
4
|
import { ConfigInvalid, EnvReading } from "@btravstack/config";
|
package/dist/s3.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as StorageBackend, u as StorageService } from "./storage-CfN3KqWm.mjs";
|
|
2
2
|
import { Module, Scope } from "@btravstack/di";
|
|
3
3
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
4
|
import { ConfigInvalid, EnvReading } from "@btravstack/config";
|
package/dist/s3.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as StorageBackend,
|
|
1
|
+
import { a as ObjectNotFound, c as StorageBackend, i as ObjectExists, l as StorageUnavailable, n as ChecksumMismatch, t as validateOptions } from "./options-DYPXkWb9.mjs";
|
|
2
2
|
import { Module, Port, Provider } from "@btravstack/di";
|
|
3
3
|
import { Ok, fromPromise } from "unthrown";
|
|
4
4
|
import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
@@ -36,6 +36,7 @@ const unavailable = (operation, key, cause) => new StorageUnavailable({
|
|
|
36
36
|
});
|
|
37
37
|
/** S3's answer to `If-None-Match: *` on a key that is held: HTTP 412. */
|
|
38
38
|
const isPreconditionFailed = (cause) => cause?.$metadata?.httpStatusCode === 412;
|
|
39
|
+
const isChecksumMismatch = (cause) => cause instanceof Error && (cause.name === "BadDigest" || cause.name === "InvalidDigest");
|
|
39
40
|
/** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
|
|
40
41
|
const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
|
|
41
42
|
/** SSE-KMS parameters when a key is named; none otherwise, so the bucket's default applies. */
|
|
@@ -52,21 +53,33 @@ const base64ToHex = (base64) => Buffer.from(base64, "base64").toString("hex");
|
|
|
52
53
|
* fiction layered over it.
|
|
53
54
|
*/
|
|
54
55
|
const s3StorageBackend = (client, bucket) => ({
|
|
55
|
-
put: (key, bytes, options) =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
put: (key, bytes, options) => {
|
|
57
|
+
const validated = validateOptions(key, options);
|
|
58
|
+
if (!validated.isOk()) return validated.toAsync();
|
|
59
|
+
return fromPromise(client.send(new PutObjectCommand({
|
|
60
|
+
Bucket: bucket,
|
|
61
|
+
Key: key,
|
|
62
|
+
Body: bytes,
|
|
63
|
+
ContentType: options.contentType,
|
|
64
|
+
Metadata: { ...options.metadata },
|
|
65
|
+
...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) },
|
|
66
|
+
...encryption(options)
|
|
67
|
+
})), (cause) => isChecksumMismatch(cause) ? new ChecksumMismatch({ key }) : unavailable("put", key, cause)).map(() => void 0);
|
|
68
|
+
},
|
|
69
|
+
putIfAbsent: (key, bytes, options) => {
|
|
70
|
+
const validated = validateOptions(key, options);
|
|
71
|
+
if (!validated.isOk()) return validated.toAsync();
|
|
72
|
+
return fromPromise(client.send(new PutObjectCommand({
|
|
73
|
+
Bucket: bucket,
|
|
74
|
+
Key: key,
|
|
75
|
+
Body: bytes,
|
|
76
|
+
ContentType: options.contentType,
|
|
77
|
+
IfNoneMatch: "*",
|
|
78
|
+
Metadata: { ...options.metadata },
|
|
79
|
+
...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) },
|
|
80
|
+
...encryption(options)
|
|
81
|
+
})), (cause) => isChecksumMismatch(cause) ? new ChecksumMismatch({ key }) : isPreconditionFailed(cause) ? new ObjectExists({ key }) : unavailable("putIfAbsent", key, cause)).map(() => void 0);
|
|
82
|
+
},
|
|
70
83
|
get: (key) => fromPromise(client.send(new GetObjectCommand({
|
|
71
84
|
Bucket: bucket,
|
|
72
85
|
Key: key
|
|
@@ -74,24 +87,31 @@ const s3StorageBackend = (client, bucket) => ({
|
|
|
74
87
|
bytes: await response.Body.transformToByteArray(),
|
|
75
88
|
contentType: response.ContentType ?? "application/octet-stream"
|
|
76
89
|
})), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("get", key, cause)),
|
|
77
|
-
copy: (from, to, options) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
copy: (from, to, options) => {
|
|
91
|
+
const validated = validateOptions(to, options);
|
|
92
|
+
if (!validated.isOk()) return validated.toAsync();
|
|
93
|
+
return fromPromise((async () => client.send(new CopyObjectCommand({
|
|
94
|
+
Bucket: bucket,
|
|
95
|
+
Key: to,
|
|
96
|
+
CopySource: `${bucket}/${from.split("/").map(encodeURIComponent).join("/")}`,
|
|
97
|
+
MetadataDirective: "REPLACE",
|
|
98
|
+
TaggingDirective: "REPLACE",
|
|
99
|
+
AnnotationDirective: "EXCLUDE",
|
|
100
|
+
ContentType: options.contentType,
|
|
101
|
+
Metadata: { ...options.metadata },
|
|
102
|
+
...options.checksum === void 0 ? {} : { ChecksumAlgorithm: "SHA256" },
|
|
103
|
+
...encryption(options)
|
|
104
|
+
})))(), (cause) => isMissing(cause) ? new ObjectNotFound({ key: from }) : unavailable("copy", from, cause)).map(() => void 0);
|
|
105
|
+
},
|
|
87
106
|
head: (key) => fromPromise(client.send(new HeadObjectCommand({
|
|
88
107
|
Bucket: bucket,
|
|
89
108
|
Key: key,
|
|
90
109
|
ChecksumMode: "ENABLED"
|
|
91
110
|
})).then((response) => ({
|
|
111
|
+
metadata: { ...response.Metadata },
|
|
92
112
|
size: response.ContentLength ?? 0,
|
|
93
113
|
contentType: response.ContentType ?? "application/octet-stream",
|
|
94
|
-
...response.ChecksumSHA256 === void 0 || response.ChecksumType === "COMPOSITE" ? {} : { sha256: base64ToHex(response.ChecksumSHA256) }
|
|
114
|
+
...response.ChecksumSHA256 === void 0 || response.ChecksumType === "COMPOSITE" || !/^[A-Za-z0-9+/]{43}=$/.test(response.ChecksumSHA256) ? {} : { sha256: base64ToHex(response.ChecksumSHA256) }
|
|
95
115
|
})), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("head", key, cause)),
|
|
96
116
|
delete: (key) => fromPromise(client.send(new DeleteObjectCommand({
|
|
97
117
|
Bucket: bucket,
|
|
@@ -101,17 +121,26 @@ const s3StorageBackend = (client, bucket) => ({
|
|
|
101
121
|
Bucket: bucket,
|
|
102
122
|
Key: key
|
|
103
123
|
}), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause)),
|
|
104
|
-
presignedUpload: (key, options) =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
presignedUpload: (key, options) => {
|
|
125
|
+
const command = new PutObjectCommand({
|
|
126
|
+
Bucket: bucket,
|
|
127
|
+
Key: key,
|
|
128
|
+
ContentType: options.contentType,
|
|
129
|
+
ContentLength: options.contentLength,
|
|
130
|
+
...encryption(options),
|
|
131
|
+
...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) }
|
|
132
|
+
});
|
|
133
|
+
const expiresIn = Math.ceil(options.ttlMs / 1e3);
|
|
134
|
+
return validateOptions(key, options).toAsync().flatMap(() => fromPromise(getSignedUrl(client, command, {
|
|
135
|
+
expiresIn,
|
|
136
|
+
signableHeaders: /* @__PURE__ */ new Set(["content-type"]),
|
|
137
|
+
unhoistableHeaders: /* @__PURE__ */ new Set([
|
|
138
|
+
"x-amz-checksum-sha256",
|
|
139
|
+
"x-amz-server-side-encryption",
|
|
140
|
+
"x-amz-server-side-encryption-aws-kms-key-id"
|
|
141
|
+
])
|
|
142
|
+
}), (cause) => unavailable("presignedUpload", key, cause)));
|
|
143
|
+
}
|
|
115
144
|
});
|
|
116
145
|
/**
|
|
117
146
|
* The S3 adapter: one client, built with the scope and destroyed with it, and
|
|
@@ -15,6 +15,17 @@ declare const ObjectExists_base: import("unthrown").TaggedErrorConstructor<"Obje
|
|
|
15
15
|
declare class ObjectExists extends ObjectExists_base<{
|
|
16
16
|
readonly key: string;
|
|
17
17
|
}> {}
|
|
18
|
+
declare const InvalidStorageOptions_base: import("unthrown").TaggedErrorConstructor<"InvalidStorageOptions">;
|
|
19
|
+
/** Invalid options are refused before any write or signature is attempted. */
|
|
20
|
+
declare class InvalidStorageOptions extends InvalidStorageOptions_base<{
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly reason: "invalid_sha256" | "invalid_metadata_key";
|
|
23
|
+
}> {}
|
|
24
|
+
declare const ChecksumMismatch_base: import("unthrown").TaggedErrorConstructor<"ChecksumMismatch">;
|
|
25
|
+
/** The supplied SHA-256 did not match the body; no object was written. */
|
|
26
|
+
declare class ChecksumMismatch extends ChecksumMismatch_base<{
|
|
27
|
+
readonly key: string;
|
|
28
|
+
}> {}
|
|
18
29
|
declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
|
|
19
30
|
/** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
20
31
|
declare class StorageUnavailable extends StorageUnavailable_base<{
|
|
@@ -30,8 +41,10 @@ declare class PresignNotSupported extends PresignNotSupported_base<{
|
|
|
30
41
|
/** What the store knows about an object without sending its bytes. */
|
|
31
42
|
type ObjectHead = {
|
|
32
43
|
readonly size: number;
|
|
44
|
+
/** An owned snapshot of user metadata; empty when none was written. */
|
|
45
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
33
46
|
readonly contentType: string;
|
|
34
|
-
/** Lowercase hex of
|
|
47
|
+
/** Lowercase hex of a full-object SHA-256; absent when the store holds none. Composite multipart checksums are excluded. */
|
|
35
48
|
readonly sha256?: string;
|
|
36
49
|
};
|
|
37
50
|
/**
|
|
@@ -41,22 +54,35 @@ type ObjectHead = {
|
|
|
41
54
|
type WriteOptions = {
|
|
42
55
|
readonly contentType: string;
|
|
43
56
|
readonly kmsKeyId?: string;
|
|
57
|
+
/** Lowercase, 64-character hex. The store verifies the body before publishing it. */
|
|
58
|
+
readonly sha256?: string;
|
|
59
|
+
/** Lowercase ASCII HTTP-token keys, without `x-amz-meta-`. No normalization; uppercase keys (and thus case collisions) are refused. The caller respects S3's 2 KB UTF-8 key/value limit. */
|
|
60
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
61
|
+
readonly checksum?: never;
|
|
62
|
+
};
|
|
63
|
+
/** A destination description and optional server-computed checksum; no write digest is accepted. */
|
|
64
|
+
type CopyOptions = {
|
|
65
|
+
readonly contentType: string;
|
|
66
|
+
readonly kmsKeyId?: string;
|
|
67
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
68
|
+
readonly checksum?: "sha256";
|
|
69
|
+
readonly sha256?: never;
|
|
44
70
|
};
|
|
45
71
|
type StorageService = {
|
|
46
|
-
readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, StorageUnavailable>;
|
|
72
|
+
readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
|
|
47
73
|
/** Writes the object only if no object holds the key: the first writer wins, atomically, in the store. */
|
|
48
|
-
readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | StorageUnavailable>;
|
|
74
|
+
readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
|
|
49
75
|
readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
|
|
50
76
|
/**
|
|
51
77
|
* Copies `from` to `to` inside the store: no bytes pass through this process.
|
|
52
|
-
* The copy
|
|
78
|
+
* The copy replaces metadata (empty when omitted) and `contentType`, and, with
|
|
53
79
|
* `kmsKeyId`, is encrypted with that key. An existing `to` is overwritten.
|
|
54
80
|
*/
|
|
55
|
-
readonly copy: (from: string, to: string, options:
|
|
81
|
+
readonly copy: (from: string, to: string, options: CopyOptions) => AsyncResult<void, ObjectNotFound | InvalidStorageOptions | StorageUnavailable>;
|
|
56
82
|
/**
|
|
57
83
|
* Size, type and checksum of an object, without reading it. `sha256` is
|
|
58
84
|
* present only if the store holds one: S3 keeps it for an object written with
|
|
59
|
-
* a SHA-256 checksum (
|
|
85
|
+
* a SHA-256 checksum (write/presign `sha256`, or copy `checksum`), and
|
|
60
86
|
* reports it base64-encoded, which the adapter converts to hex.
|
|
61
87
|
*/
|
|
62
88
|
readonly head: (key: string) => AsyncResult<ObjectHead, ObjectNotFound | StorageUnavailable>;
|
|
@@ -91,6 +117,11 @@ type StorageService = {
|
|
|
91
117
|
* hash to it, and keeps it for `head`. The client must send the same value as
|
|
92
118
|
* `x-amz-checksum-sha256` (base64 of the digest).
|
|
93
119
|
*
|
|
120
|
+
* With `kmsKeyId`, both `x-amz-server-side-encryption: aws:kms` and
|
|
121
|
+
* `x-amz-server-side-encryption-aws-kms-key-id` are mandatory signed headers.
|
|
122
|
+
* The client sends the exact key id used here. Omitting or changing either
|
|
123
|
+
* breaks the signature. Malformed `sha256` returns `InvalidStorageOptions`.
|
|
124
|
+
*
|
|
94
125
|
* The application still decides every input: whether this caller may write
|
|
95
126
|
* this key, how long the URL lives, and what size it is willing to accept.
|
|
96
127
|
* The adapter computes a signature over that decision and nothing more.
|
|
@@ -100,7 +131,8 @@ type StorageService = {
|
|
|
100
131
|
readonly contentType: string;
|
|
101
132
|
readonly contentLength: number;
|
|
102
133
|
readonly sha256?: string;
|
|
103
|
-
|
|
134
|
+
readonly kmsKeyId?: string;
|
|
135
|
+
}) => AsyncResult<string, PresignNotSupported | InvalidStorageOptions | StorageUnavailable>;
|
|
104
136
|
};
|
|
105
137
|
declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
|
|
106
138
|
/**
|
|
@@ -119,4 +151,4 @@ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBa
|
|
|
119
151
|
/** The port every adapter provides, and the one an application never depends on. */
|
|
120
152
|
declare class StorageBackend extends StorageBackend_base<StorageService> {}
|
|
121
153
|
//#endregion
|
|
122
|
-
export {
|
|
154
|
+
export { ObjectHead as a, Storage as c, StorageUnavailable as d, StoredObject as f, ObjectExists as i, StorageBackend as l, CopyOptions as n, ObjectNotFound as o, WriteOptions as p, InvalidStorageOptions as r, PresignNotSupported as s, ChecksumMismatch as t, StorageService as u };
|
|
@@ -15,6 +15,17 @@ declare const ObjectExists_base: import("unthrown").TaggedErrorConstructor<"Obje
|
|
|
15
15
|
declare class ObjectExists extends ObjectExists_base<{
|
|
16
16
|
readonly key: string;
|
|
17
17
|
}> {}
|
|
18
|
+
declare const InvalidStorageOptions_base: import("unthrown").TaggedErrorConstructor<"InvalidStorageOptions">;
|
|
19
|
+
/** Invalid options are refused before any write or signature is attempted. */
|
|
20
|
+
declare class InvalidStorageOptions extends InvalidStorageOptions_base<{
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly reason: "invalid_sha256" | "invalid_metadata_key";
|
|
23
|
+
}> {}
|
|
24
|
+
declare const ChecksumMismatch_base: import("unthrown").TaggedErrorConstructor<"ChecksumMismatch">;
|
|
25
|
+
/** The supplied SHA-256 did not match the body; no object was written. */
|
|
26
|
+
declare class ChecksumMismatch extends ChecksumMismatch_base<{
|
|
27
|
+
readonly key: string;
|
|
28
|
+
}> {}
|
|
18
29
|
declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
|
|
19
30
|
/** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
20
31
|
declare class StorageUnavailable extends StorageUnavailable_base<{
|
|
@@ -30,8 +41,10 @@ declare class PresignNotSupported extends PresignNotSupported_base<{
|
|
|
30
41
|
/** What the store knows about an object without sending its bytes. */
|
|
31
42
|
type ObjectHead = {
|
|
32
43
|
readonly size: number;
|
|
44
|
+
/** An owned snapshot of user metadata; empty when none was written. */
|
|
45
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
33
46
|
readonly contentType: string;
|
|
34
|
-
/** Lowercase hex of
|
|
47
|
+
/** Lowercase hex of a full-object SHA-256; absent when the store holds none. Composite multipart checksums are excluded. */
|
|
35
48
|
readonly sha256?: string;
|
|
36
49
|
};
|
|
37
50
|
/**
|
|
@@ -41,22 +54,35 @@ type ObjectHead = {
|
|
|
41
54
|
type WriteOptions = {
|
|
42
55
|
readonly contentType: string;
|
|
43
56
|
readonly kmsKeyId?: string;
|
|
57
|
+
/** Lowercase, 64-character hex. The store verifies the body before publishing it. */
|
|
58
|
+
readonly sha256?: string;
|
|
59
|
+
/** Lowercase ASCII HTTP-token keys, without `x-amz-meta-`. No normalization; uppercase keys (and thus case collisions) are refused. The caller respects S3's 2 KB UTF-8 key/value limit. */
|
|
60
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
61
|
+
readonly checksum?: never;
|
|
62
|
+
};
|
|
63
|
+
/** A destination description and optional server-computed checksum; no write digest is accepted. */
|
|
64
|
+
type CopyOptions = {
|
|
65
|
+
readonly contentType: string;
|
|
66
|
+
readonly kmsKeyId?: string;
|
|
67
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
68
|
+
readonly checksum?: "sha256";
|
|
69
|
+
readonly sha256?: never;
|
|
44
70
|
};
|
|
45
71
|
type StorageService = {
|
|
46
|
-
readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, StorageUnavailable>;
|
|
72
|
+
readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
|
|
47
73
|
/** Writes the object only if no object holds the key: the first writer wins, atomically, in the store. */
|
|
48
|
-
readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | StorageUnavailable>;
|
|
74
|
+
readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
|
|
49
75
|
readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
|
|
50
76
|
/**
|
|
51
77
|
* Copies `from` to `to` inside the store: no bytes pass through this process.
|
|
52
|
-
* The copy
|
|
78
|
+
* The copy replaces metadata (empty when omitted) and `contentType`, and, with
|
|
53
79
|
* `kmsKeyId`, is encrypted with that key. An existing `to` is overwritten.
|
|
54
80
|
*/
|
|
55
|
-
readonly copy: (from: string, to: string, options:
|
|
81
|
+
readonly copy: (from: string, to: string, options: CopyOptions) => AsyncResult<void, ObjectNotFound | InvalidStorageOptions | StorageUnavailable>;
|
|
56
82
|
/**
|
|
57
83
|
* Size, type and checksum of an object, without reading it. `sha256` is
|
|
58
84
|
* present only if the store holds one: S3 keeps it for an object written with
|
|
59
|
-
* a SHA-256 checksum (
|
|
85
|
+
* a SHA-256 checksum (write/presign `sha256`, or copy `checksum`), and
|
|
60
86
|
* reports it base64-encoded, which the adapter converts to hex.
|
|
61
87
|
*/
|
|
62
88
|
readonly head: (key: string) => AsyncResult<ObjectHead, ObjectNotFound | StorageUnavailable>;
|
|
@@ -91,6 +117,11 @@ type StorageService = {
|
|
|
91
117
|
* hash to it, and keeps it for `head`. The client must send the same value as
|
|
92
118
|
* `x-amz-checksum-sha256` (base64 of the digest).
|
|
93
119
|
*
|
|
120
|
+
* With `kmsKeyId`, both `x-amz-server-side-encryption: aws:kms` and
|
|
121
|
+
* `x-amz-server-side-encryption-aws-kms-key-id` are mandatory signed headers.
|
|
122
|
+
* The client sends the exact key id used here. Omitting or changing either
|
|
123
|
+
* breaks the signature. Malformed `sha256` returns `InvalidStorageOptions`.
|
|
124
|
+
*
|
|
94
125
|
* The application still decides every input: whether this caller may write
|
|
95
126
|
* this key, how long the URL lives, and what size it is willing to accept.
|
|
96
127
|
* The adapter computes a signature over that decision and nothing more.
|
|
@@ -100,7 +131,8 @@ type StorageService = {
|
|
|
100
131
|
readonly contentType: string;
|
|
101
132
|
readonly contentLength: number;
|
|
102
133
|
readonly sha256?: string;
|
|
103
|
-
|
|
134
|
+
readonly kmsKeyId?: string;
|
|
135
|
+
}) => AsyncResult<string, PresignNotSupported | InvalidStorageOptions | StorageUnavailable>;
|
|
104
136
|
};
|
|
105
137
|
declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
|
|
106
138
|
/**
|
|
@@ -119,4 +151,4 @@ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBa
|
|
|
119
151
|
/** The port every adapter provides, and the one an application never depends on. */
|
|
120
152
|
declare class StorageBackend extends StorageBackend_base<StorageService> {}
|
|
121
153
|
//#endregion
|
|
122
|
-
export {
|
|
154
|
+
export { ObjectHead as a, Storage as c, StorageUnavailable as d, StoredObject as f, ObjectExists as i, StorageBackend as l, CopyOptions as n, ObjectNotFound as o, WriteOptions as p, InvalidStorageOptions as r, PresignNotSupported as s, ChecksumMismatch as t, StorageService as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "A Storage port for @btravstack/core: an in-memory adapter, an S3-compatible adapter with presigned reads, and a composition that spans, counts and logs every operation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dependency-injection",
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@aws-sdk/client-s3": "3.1149.0",
|
|
57
57
|
"@aws-sdk/s3-request-presigner": "3.1149.0",
|
|
58
|
-
"@btravstack/config": "0.
|
|
59
|
-
"@btravstack/core": "0.
|
|
60
|
-
"@btravstack/di": "0.
|
|
58
|
+
"@btravstack/config": "0.39.1",
|
|
59
|
+
"@btravstack/core": "0.39.1",
|
|
60
|
+
"@btravstack/di": "0.39.1",
|
|
61
61
|
"@btravstack/internal-test-infra": "0.0.0",
|
|
62
|
-
"@btravstack/observability": "0.
|
|
62
|
+
"@btravstack/observability": "0.39.1",
|
|
63
63
|
"@btravstack/tsconfig": "0.4.0",
|
|
64
64
|
"@opentelemetry/api": "1.9.1",
|
|
65
65
|
"@opentelemetry/sdk-metrics": "2.12.0",
|
|
@@ -76,9 +76,9 @@
|
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"@aws-sdk/client-s3": "^3.1000.0",
|
|
78
78
|
"@aws-sdk/s3-request-presigner": "^3.1000.0",
|
|
79
|
-
"@btravstack/config": "^0.
|
|
80
|
-
"@btravstack/core": "^0.
|
|
81
|
-
"@btravstack/di": "^0.
|
|
79
|
+
"@btravstack/config": "^0.39.1",
|
|
80
|
+
"@btravstack/core": "^0.39.1",
|
|
81
|
+
"@btravstack/di": "^0.39.1",
|
|
82
82
|
"unthrown": "^5.0.0"
|
|
83
83
|
},
|
|
84
84
|
"peerDependenciesMeta": {
|