@btravstack/storage 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- | `adapter` | `storage({ adapter })` | the adapter module providing `StorageBackend` — required |
112
- | `STORAGE_S3_ENDPOINT` | environment, read by `s3Storage()` | the store's endpoint — required |
113
- | `STORAGE_S3_BUCKET` | environment | the bucket — required |
114
- | `STORAGE_S3_ACCESS_KEY_ID` / `STORAGE_S3_SECRET_ACCESS_KEY` | environment | the credentials — required |
115
- | `STORAGE_S3_REGION` | environment | default `us-east-1` |
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`, logged at `info`), that a held key under `putIfAbsent` is an ordinary answer (`ObjectExists`, counted `exists`), that `delete` is idempotent, that
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,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_storage = require("./storage-DlAQXJ1t.cjs");
2
+ const require_options = require("./options-BhML_i5p.cjs");
3
+ let node_crypto = require("node:crypto");
3
4
  let _btravstack_di = require("@btravstack/di");
4
5
  let unthrown = require("unthrown");
5
6
  let _btravstack_core = require("@btravstack/core");
@@ -11,48 +12,74 @@ let _btravstack_core = require("@btravstack/core");
11
12
  * which would be the worst kind of double — one that passes locally and fails in
12
13
  * the deployment for a reason no test could have shown.
13
14
  *
15
+ * `kmsKeyId` is accepted and ignored: nothing is encrypted at rest here.
16
+ *
14
17
  * ponytail: no size limit, so a process storing unbounded objects grows
15
18
  * unbounded. The upgrade path is the S3 adapter.
16
19
  */
17
20
  const memoryStorageBackend = () => {
18
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)());
19
23
  return {
20
- put: (key, bytes, options) => {
24
+ put: (key, bytes, options) => verify(key, bytes, options).map(() => {
21
25
  objects.set(key, {
22
- bytes,
23
- contentType: options.contentType
26
+ bytes: Uint8Array.from(bytes),
27
+ contentType: options.contentType,
28
+ metadata: { ...options.metadata }
24
29
  });
25
- return (0, unthrown.OkAsync)();
26
- },
27
- putIfAbsent: (key, bytes, options) => {
28
- 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 }));
29
33
  objects.set(key, {
30
- bytes,
31
- contentType: options.contentType
34
+ bytes: Uint8Array.from(bytes),
35
+ contentType: options.contentType,
36
+ metadata: { ...options.metadata }
37
+ });
38
+ return (0, unthrown.Ok)();
39
+ }).toAsync(),
40
+ copy: (from, to, options) => require_options.validateOptions(to, options).flatMap(() => {
41
+ const object = objects.get(from);
42
+ if (object === void 0) return (0, unthrown.Err)(new require_options.ObjectNotFound({ key: from }));
43
+ objects.set(to, {
44
+ bytes: object.bytes.slice(),
45
+ contentType: options.contentType,
46
+ metadata: { ...options.metadata }
47
+ });
48
+ return (0, unthrown.Ok)();
49
+ }).toAsync(),
50
+ head: (key) => {
51
+ const object = objects.get(key);
52
+ return object === void 0 ? (0, unthrown.ErrAsync)(new require_options.ObjectNotFound({ key })) : (0, unthrown.OkAsync)({
53
+ metadata: { ...object.metadata },
54
+ size: object.bytes.byteLength,
55
+ contentType: object.contentType,
56
+ sha256: (0, node_crypto.createHash)("sha256").update(object.bytes).digest("hex")
32
57
  });
33
- return (0, unthrown.OkAsync)();
34
58
  },
35
59
  get: (key) => {
36
60
  const object = objects.get(key);
37
- return object === void 0 ? (0, unthrown.ErrAsync)(new require_storage.ObjectNotFound({ key })) : (0, unthrown.OkAsync)(object);
61
+ return object === void 0 ? (0, unthrown.ErrAsync)(new require_options.ObjectNotFound({ key })) : (0, unthrown.OkAsync)({
62
+ contentType: object.contentType,
63
+ bytes: object.bytes.slice()
64
+ });
38
65
  },
39
66
  delete: (key) => {
40
67
  objects.delete(key);
41
68
  return (0, unthrown.OkAsync)();
42
69
  },
43
- presignedUrl: (key) => (0, unthrown.ErrAsync)(new require_storage.PresignNotSupported({ key })),
44
- presignedUpload: (key) => (0, unthrown.ErrAsync)(new require_storage.PresignNotSupported({ key }))
70
+ presignedUrl: (key) => (0, unthrown.ErrAsync)(new require_options.PresignNotSupported({ key })),
71
+ presignedUpload: (key) => (0, unthrown.ErrAsync)(new require_options.PresignNotSupported({ key }))
45
72
  };
46
73
  };
47
74
  /** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
48
- const memoryStorageProvider = () => (0, _btravstack_di.Provider)(require_storage.StorageBackend)({
75
+ const memoryStorageProvider = () => (0, _btravstack_di.Provider)(require_options.StorageBackend)({
49
76
  inject: {},
50
77
  sync: () => memoryStorageBackend()
51
78
  });
52
79
  /** The adapter as a module, which is the shape `storage({ adapter })` takes. */
53
80
  const memoryStorage = () => (0, _btravstack_di.Module)("MemoryStorage")({
54
81
  provides: [memoryStorageProvider()],
55
- exports: [require_storage.StorageBackend]
82
+ exports: [require_options.StorageBackend]
56
83
  });
57
84
  //#endregion
58
85
  //#region src/instrument.ts
@@ -60,8 +87,8 @@ const memoryStorage = () => (0, _btravstack_di.Module)("MemoryStorage")({
60
87
  * **A missing object settles `ok` with `result: "not_found"`, not `error`.**
61
88
  * Asking for something that is not there is an ordinary answer, and a dashboard
62
89
  * that treats it as a fault teaches its readers to ignore the fault line.
63
- * `StorageUnavailable` is what pages somebody, and it is the only one that
64
- * settles as an error. The two ordinary answers keep their own words in
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
65
92
  * `reason`, so an operator is not left hunting an object sitting where they put
66
93
  * it.
67
94
  */
@@ -103,9 +130,29 @@ const through = (observers) => (operation, key, call) => (0, _btravstack_core.ob
103
130
  const instrument = (backend, observers) => {
104
131
  const observe = through(observers);
105
132
  return {
106
- put: (key, bytes, options) => observe("put", key, () => backend.put(key, bytes, options)),
107
- putIfAbsent: (key, bytes, options) => observe("put_if_absent", key, () => backend.putIfAbsent(key, bytes, options)),
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
+ },
108
147
  get: (key) => observe("get", key, () => backend.get(key)),
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
+ },
155
+ head: (key) => observe("head", key, () => backend.head(key)),
109
156
  delete: (key) => observe("delete", key, () => backend.delete(key)),
110
157
  presignedUrl: (key, options) => observe("presigned_url", key, () => backend.presignedUrl(key, options)),
111
158
  presignedUpload: (key, options) => observe("presigned_upload", key, () => backend.presignedUpload(key, options))
@@ -131,7 +178,7 @@ const instrument = (backend, observers) => {
131
178
  */
132
179
  const storage = ({ adapter }) => {
133
180
  const healthCheck = _btravstack_di.Provider.member(_btravstack_core.HealthChecks)({
134
- inject: { backend: require_storage.StorageBackend },
181
+ inject: { backend: require_options.StorageBackend },
135
182
  sync: ({ backend }) => ({
136
183
  name: "storage",
137
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})` }))))
@@ -141,25 +188,27 @@ const storage = ({ adapter }) => {
141
188
  imports: [adapter],
142
189
  provides: [
143
190
  _btravstack_core.noObserverMember,
144
- (0, _btravstack_di.Provider)(require_storage.Storage)({
191
+ (0, _btravstack_di.Provider)(require_options.Storage)({
145
192
  inject: {
146
- backend: require_storage.StorageBackend,
193
+ backend: require_options.StorageBackend,
147
194
  observers: _btravstack_core.Observers
148
195
  },
149
196
  sync: ({ backend, observers }) => instrument(backend, observers)
150
197
  }),
151
198
  healthCheck
152
199
  ],
153
- exports: [require_storage.Storage, _btravstack_core.HealthChecks]
200
+ exports: [require_options.Storage, _btravstack_core.HealthChecks]
154
201
  });
155
202
  };
156
203
  //#endregion
157
- exports.ObjectExists = require_storage.ObjectExists;
158
- exports.ObjectNotFound = require_storage.ObjectNotFound;
159
- exports.PresignNotSupported = require_storage.PresignNotSupported;
160
- exports.Storage = require_storage.Storage;
161
- exports.StorageBackend = require_storage.StorageBackend;
162
- exports.StorageUnavailable = require_storage.StorageUnavailable;
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;
163
212
  exports.memoryStorage = memoryStorage;
164
213
  exports.memoryStorageBackend = memoryStorageBackend;
165
214
  exports.memoryStorageProvider = memoryStorageProvider;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as StorageBackend, c as StoredObject, i as Storage, n as ObjectNotFound, o as StorageService, r as PresignNotSupported, s as StorageUnavailable, t as ObjectExists } from "./storage-uSBWFyB2.cjs";
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
@@ -9,6 +9,8 @@ import { HealthChecks } from "@btravstack/core";
9
9
  * which would be the worst kind of double — one that passes locally and fails in
10
10
  * the deployment for a reason no test could have shown.
11
11
  *
12
+ * `kmsKeyId` is accepted and ignored: nothing is encrypted at rest here.
13
+ *
12
14
  * ponytail: no size limit, so a process storing unbounded objects grows
13
15
  * unbounded. The upgrade path is the S3 adapter.
14
16
  */
@@ -47,4 +49,4 @@ type StorageOptions<E, N> = {
47
49
  */
48
50
  export declare const storage: <E, N>({ adapter }: StorageOptions<E, N>) => Module<Storage | HealthChecks, E, N>;
49
51
  //#endregion
50
- export { ObjectExists, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject };
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 StorageBackend, c as StoredObject, i as Storage, n as ObjectNotFound, o as StorageService, r as PresignNotSupported, s as StorageUnavailable, t as ObjectExists } from "./storage-uSBWFyB2.mjs";
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
@@ -9,6 +9,8 @@ import { HealthChecks } from "@btravstack/core";
9
9
  * which would be the worst kind of double — one that passes locally and fails in
10
10
  * the deployment for a reason no test could have shown.
11
11
  *
12
+ * `kmsKeyId` is accepted and ignored: nothing is encrypted at rest here.
13
+ *
12
14
  * ponytail: no size limit, so a process storing unbounded objects grows
13
15
  * unbounded. The upgrade path is the S3 adapter.
14
16
  */
@@ -47,4 +49,4 @@ type StorageOptions<E, N> = {
47
49
  */
48
50
  export declare const storage: <E, N>({ adapter }: StorageOptions<E, N>) => Module<Storage | HealthChecks, E, N>;
49
51
  //#endregion
50
- export { ObjectExists, ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject };
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,5 @@
1
- import { a as StorageBackend, i as Storage, n as ObjectNotFound, o as StorageUnavailable, r as PresignNotSupported, t as ObjectExists } from "./storage-BmbyYPIE.mjs";
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
+ import { createHash } from "node:crypto";
2
3
  import { Module, Provider } from "@btravstack/di";
3
4
  import { Err, ErrAsync, Ok, OkAsync, P } from "unthrown";
4
5
  import { HealthCheckFailed, HealthChecks, Observers, noObserverMember, observed } from "@btravstack/core";
@@ -10,30 +11,56 @@ import { HealthCheckFailed, HealthChecks, Observers, noObserverMember, observed
10
11
  * which would be the worst kind of double — one that passes locally and fails in
11
12
  * the deployment for a reason no test could have shown.
12
13
  *
14
+ * `kmsKeyId` is accepted and ignored: nothing is encrypted at rest here.
15
+ *
13
16
  * ponytail: no size limit, so a process storing unbounded objects grows
14
17
  * unbounded. The upgrade path is the S3 adapter.
15
18
  */
16
19
  const memoryStorageBackend = () => {
17
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());
18
22
  return {
19
- put: (key, bytes, options) => {
23
+ put: (key, bytes, options) => verify(key, bytes, options).map(() => {
20
24
  objects.set(key, {
21
- bytes,
22
- contentType: options.contentType
25
+ bytes: Uint8Array.from(bytes),
26
+ contentType: options.contentType,
27
+ metadata: { ...options.metadata }
23
28
  });
24
- return OkAsync();
25
- },
26
- putIfAbsent: (key, bytes, options) => {
27
- 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 }));
28
32
  objects.set(key, {
29
- bytes,
30
- contentType: options.contentType
33
+ bytes: Uint8Array.from(bytes),
34
+ contentType: options.contentType,
35
+ metadata: { ...options.metadata }
36
+ });
37
+ return Ok();
38
+ }).toAsync(),
39
+ copy: (from, to, options) => validateOptions(to, options).flatMap(() => {
40
+ const object = objects.get(from);
41
+ if (object === void 0) return Err(new ObjectNotFound({ key: from }));
42
+ objects.set(to, {
43
+ bytes: object.bytes.slice(),
44
+ contentType: options.contentType,
45
+ metadata: { ...options.metadata }
46
+ });
47
+ return Ok();
48
+ }).toAsync(),
49
+ head: (key) => {
50
+ const object = objects.get(key);
51
+ return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync({
52
+ metadata: { ...object.metadata },
53
+ size: object.bytes.byteLength,
54
+ contentType: object.contentType,
55
+ sha256: createHash("sha256").update(object.bytes).digest("hex")
31
56
  });
32
- return OkAsync();
33
57
  },
34
58
  get: (key) => {
35
59
  const object = objects.get(key);
36
- return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync(object);
60
+ return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync({
61
+ contentType: object.contentType,
62
+ bytes: object.bytes.slice()
63
+ });
37
64
  },
38
65
  delete: (key) => {
39
66
  objects.delete(key);
@@ -59,8 +86,8 @@ const memoryStorage = () => Module("MemoryStorage")({
59
86
  * **A missing object settles `ok` with `result: "not_found"`, not `error`.**
60
87
  * Asking for something that is not there is an ordinary answer, and a dashboard
61
88
  * that treats it as a fault teaches its readers to ignore the fault line.
62
- * `StorageUnavailable` is what pages somebody, and it is the only one that
63
- * settles as an error. The two ordinary answers keep their own words in
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
64
91
  * `reason`, so an operator is not left hunting an object sitting where they put
65
92
  * it.
66
93
  */
@@ -102,9 +129,29 @@ const through = (observers) => (operation, key, call) => observed(observers, {
102
129
  const instrument = (backend, observers) => {
103
130
  const observe = through(observers);
104
131
  return {
105
- put: (key, bytes, options) => observe("put", key, () => backend.put(key, bytes, options)),
106
- putIfAbsent: (key, bytes, options) => observe("put_if_absent", key, () => backend.putIfAbsent(key, bytes, options)),
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
+ },
107
146
  get: (key) => observe("get", key, () => backend.get(key)),
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
+ },
154
+ head: (key) => observe("head", key, () => backend.head(key)),
108
155
  delete: (key) => observe("delete", key, () => backend.delete(key)),
109
156
  presignedUrl: (key, options) => observe("presigned_url", key, () => backend.presignedUrl(key, options)),
110
157
  presignedUpload: (key, options) => observe("presigned_upload", key, () => backend.presignedUpload(key, options))
@@ -153,4 +200,4 @@ const storage = ({ adapter }) => {
153
200
  });
154
201
  };
155
202
  //#endregion
156
- 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 };
@@ -1,10 +1,14 @@
1
1
  let _btravstack_di = require("@btravstack/di");
2
2
  let unthrown = require("unthrown");
3
3
  //#region src/storage.ts
4
- /** The key is not there. Only `get` answers it — see `delete` and `presignedUrl` below. */
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 (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
- /** The key is not there. Only `get` answers it — see `delete` and `presignedUrl` below. */
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
- export { StorageBackend as a, Storage as i, ObjectNotFound as n, StorageUnavailable as o, PresignNotSupported as r, ObjectExists as t };
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 };