@btravstack/storage 0.37.0 → 0.38.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
@@ -121,7 +121,7 @@ which is this list's one detailed home.
121
121
  ## What it decides, and what it does not
122
122
 
123
123
  **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
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 `copy` and `head` never move the bytes through the process, that
125
125
  presigning asks the store nothing — and that an adapter which cannot presign
126
126
  says so instead of minting a URL that would fail only in production.
127
127
 
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_storage = require("./storage-EA671z1R.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,6 +12,8 @@ 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
  */
@@ -19,7 +22,7 @@ const memoryStorageBackend = () => {
19
22
  return {
20
23
  put: (key, bytes, options) => {
21
24
  objects.set(key, {
22
- bytes,
25
+ bytes: bytes.slice(),
23
26
  contentType: options.contentType
24
27
  });
25
28
  return (0, unthrown.OkAsync)();
@@ -27,14 +30,34 @@ const memoryStorageBackend = () => {
27
30
  putIfAbsent: (key, bytes, options) => {
28
31
  if (objects.has(key)) return (0, unthrown.ErrAsync)(new require_storage.ObjectExists({ key }));
29
32
  objects.set(key, {
30
- bytes,
33
+ bytes: bytes.slice(),
34
+ contentType: options.contentType
35
+ });
36
+ return (0, unthrown.OkAsync)();
37
+ },
38
+ copy: (from, to, options) => {
39
+ const object = objects.get(from);
40
+ if (object === void 0) return (0, unthrown.ErrAsync)(new require_storage.ObjectNotFound({ key: from }));
41
+ objects.set(to, {
42
+ bytes: object.bytes.slice(),
31
43
  contentType: options.contentType
32
44
  });
33
45
  return (0, unthrown.OkAsync)();
34
46
  },
47
+ head: (key) => {
48
+ const object = objects.get(key);
49
+ return object === void 0 ? (0, unthrown.ErrAsync)(new require_storage.ObjectNotFound({ key })) : (0, unthrown.OkAsync)({
50
+ size: object.bytes.byteLength,
51
+ contentType: object.contentType,
52
+ sha256: (0, node_crypto.createHash)("sha256").update(object.bytes).digest("hex")
53
+ });
54
+ },
35
55
  get: (key) => {
36
56
  const object = objects.get(key);
37
- return object === void 0 ? (0, unthrown.ErrAsync)(new require_storage.ObjectNotFound({ key })) : (0, unthrown.OkAsync)(object);
57
+ return object === void 0 ? (0, unthrown.ErrAsync)(new require_storage.ObjectNotFound({ key })) : (0, unthrown.OkAsync)({
58
+ ...object,
59
+ bytes: object.bytes.slice()
60
+ });
38
61
  },
39
62
  delete: (key) => {
40
63
  objects.delete(key);
@@ -106,6 +129,8 @@ const instrument = (backend, observers) => {
106
129
  put: (key, bytes, options) => observe("put", key, () => backend.put(key, bytes, options)),
107
130
  putIfAbsent: (key, bytes, options) => observe("put_if_absent", key, () => backend.putIfAbsent(key, bytes, options)),
108
131
  get: (key) => observe("get", key, () => backend.get(key)),
132
+ copy: (from, to, options) => observe("copy", from, () => backend.copy(from, to, options)),
133
+ head: (key) => observe("head", key, () => backend.head(key)),
109
134
  delete: (key) => observe("delete", key, () => backend.delete(key)),
110
135
  presignedUrl: (key, options) => observe("presigned_url", key, () => backend.presignedUrl(key, options)),
111
136
  presignedUpload: (key, options) => observe("presigned_upload", key, () => backend.presignedUpload(key, options))
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 Storage, c as StorageUnavailable, i as PresignNotSupported, l as StoredObject, n as ObjectHead, o as StorageBackend, r as ObjectNotFound, s as StorageService, t as ObjectExists, u as WriteOptions } from "./storage-BavPXZpA.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 { 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 Storage, c as StorageUnavailable, i as PresignNotSupported, l as StoredObject, n as ObjectHead, o as StorageBackend, r as ObjectNotFound, s as StorageService, t as ObjectExists, u as WriteOptions } from "./storage-BavPXZpA.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 { 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 StorageBackend, i as Storage, n as ObjectNotFound, o as StorageUnavailable, r as PresignNotSupported, t as ObjectExists } from "./storage-BIKstQ72.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,6 +11,8 @@ 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
  */
@@ -18,7 +21,7 @@ const memoryStorageBackend = () => {
18
21
  return {
19
22
  put: (key, bytes, options) => {
20
23
  objects.set(key, {
21
- bytes,
24
+ bytes: bytes.slice(),
22
25
  contentType: options.contentType
23
26
  });
24
27
  return OkAsync();
@@ -26,14 +29,34 @@ const memoryStorageBackend = () => {
26
29
  putIfAbsent: (key, bytes, options) => {
27
30
  if (objects.has(key)) return ErrAsync(new ObjectExists({ key }));
28
31
  objects.set(key, {
29
- bytes,
32
+ bytes: bytes.slice(),
33
+ contentType: options.contentType
34
+ });
35
+ return OkAsync();
36
+ },
37
+ copy: (from, to, options) => {
38
+ const object = objects.get(from);
39
+ if (object === void 0) return ErrAsync(new ObjectNotFound({ key: from }));
40
+ objects.set(to, {
41
+ bytes: object.bytes.slice(),
30
42
  contentType: options.contentType
31
43
  });
32
44
  return OkAsync();
33
45
  },
46
+ head: (key) => {
47
+ const object = objects.get(key);
48
+ return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync({
49
+ size: object.bytes.byteLength,
50
+ contentType: object.contentType,
51
+ sha256: createHash("sha256").update(object.bytes).digest("hex")
52
+ });
53
+ },
34
54
  get: (key) => {
35
55
  const object = objects.get(key);
36
- return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync(object);
56
+ return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync({
57
+ ...object,
58
+ bytes: object.bytes.slice()
59
+ });
37
60
  },
38
61
  delete: (key) => {
39
62
  objects.delete(key);
@@ -105,6 +128,8 @@ const instrument = (backend, observers) => {
105
128
  put: (key, bytes, options) => observe("put", key, () => backend.put(key, bytes, options)),
106
129
  putIfAbsent: (key, bytes, options) => observe("put_if_absent", key, () => backend.putIfAbsent(key, bytes, options)),
107
130
  get: (key) => observe("get", key, () => backend.get(key)),
131
+ copy: (from, to, options) => observe("copy", from, () => backend.copy(from, to, options)),
132
+ head: (key) => observe("head", key, () => backend.head(key)),
108
133
  delete: (key) => observe("delete", key, () => backend.delete(key)),
109
134
  presignedUrl: (key, options) => observe("presigned_url", key, () => backend.presignedUrl(key, options)),
110
135
  presignedUpload: (key, options) => observe("presigned_upload", key, () => backend.presignedUpload(key, options))
package/dist/s3.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_storage = require("./storage-DlAQXJ1t.cjs");
2
+ const require_storage = require("./storage-EA671z1R.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");
@@ -39,6 +39,14 @@ const unavailable = (operation, key, cause) => new require_storage.StorageUnavai
39
39
  const isPreconditionFailed = (cause) => cause?.$metadata?.httpStatusCode === 412;
40
40
  /** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
41
41
  const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
42
+ /** SSE-KMS parameters when a key is named; none otherwise, so the bucket's default applies. */
43
+ const encryption = (options) => options.kmsKeyId === void 0 ? {} : {
44
+ ServerSideEncryption: "aws:kms",
45
+ SSEKMSKeyId: options.kmsKeyId
46
+ };
47
+ /** S3 reports and takes checksums in base64; the port speaks hex. */
48
+ const hexToBase64 = (hex) => Buffer.from(hex, "hex").toString("base64");
49
+ const base64ToHex = (base64) => Buffer.from(base64, "base64").toString("hex");
42
50
  /**
43
51
  * The adapter's service over a client and a bucket. A delete of a key nobody
44
52
  * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
@@ -49,14 +57,16 @@ const s3StorageBackend = (client, bucket) => ({
49
57
  Bucket: bucket,
50
58
  Key: key,
51
59
  Body: bytes,
52
- ContentType: options.contentType
60
+ ContentType: options.contentType,
61
+ ...encryption(options)
53
62
  })), (cause) => unavailable("put", key, cause)).map(() => void 0),
54
63
  putIfAbsent: (key, bytes, options) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.PutObjectCommand({
55
64
  Bucket: bucket,
56
65
  Key: key,
57
66
  Body: bytes,
58
67
  ContentType: options.contentType,
59
- IfNoneMatch: "*"
68
+ IfNoneMatch: "*",
69
+ ...encryption(options)
60
70
  })), (cause) => isPreconditionFailed(cause) ? new require_storage.ObjectExists({ key }) : unavailable("putIfAbsent", key, cause)).map(() => void 0),
61
71
  get: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.GetObjectCommand({
62
72
  Bucket: bucket,
@@ -65,6 +75,25 @@ const s3StorageBackend = (client, bucket) => ({
65
75
  bytes: await response.Body.transformToByteArray(),
66
76
  contentType: response.ContentType ?? "application/octet-stream"
67
77
  })), (cause) => isMissing(cause) ? new require_storage.ObjectNotFound({ key }) : unavailable("get", key, cause)),
78
+ copy: (from, to, options) => (0, unthrown.fromPromise)((async () => client.send(new _aws_sdk_client_s3.CopyObjectCommand({
79
+ Bucket: bucket,
80
+ Key: to,
81
+ CopySource: `${bucket}/${from.split("/").map(encodeURIComponent).join("/")}`,
82
+ MetadataDirective: "REPLACE",
83
+ TaggingDirective: "REPLACE",
84
+ AnnotationDirective: "EXCLUDE",
85
+ ContentType: options.contentType,
86
+ ...encryption(options)
87
+ })))(), (cause) => isMissing(cause) ? new require_storage.ObjectNotFound({ key: from }) : unavailable("copy", from, cause)).map(() => void 0),
88
+ head: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.HeadObjectCommand({
89
+ Bucket: bucket,
90
+ Key: key,
91
+ ChecksumMode: "ENABLED"
92
+ })).then((response) => ({
93
+ size: response.ContentLength ?? 0,
94
+ contentType: response.ContentType ?? "application/octet-stream",
95
+ ...response.ChecksumSHA256 === void 0 || response.ChecksumType === "COMPOSITE" ? {} : { sha256: base64ToHex(response.ChecksumSHA256) }
96
+ })), (cause) => isMissing(cause) ? new require_storage.ObjectNotFound({ key }) : unavailable("head", key, cause)),
68
97
  delete: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
69
98
  Bucket: bucket,
70
99
  Key: key
@@ -77,10 +106,12 @@ const s3StorageBackend = (client, bucket) => ({
77
106
  Bucket: bucket,
78
107
  Key: key,
79
108
  ContentType: options.contentType,
80
- ContentLength: options.contentLength
109
+ ContentLength: options.contentLength,
110
+ ...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) }
81
111
  }), {
82
112
  expiresIn: Math.ceil(options.ttlMs / 1e3),
83
- signableHeaders: /* @__PURE__ */ new Set(["content-type"])
113
+ signableHeaders: /* @__PURE__ */ new Set(["content-type"]),
114
+ ...options.sha256 === void 0 ? {} : { unhoistableHeaders: /* @__PURE__ */ new Set(["x-amz-checksum-sha256"]) }
84
115
  }), (cause) => unavailable("presignedUpload", key, cause))
85
116
  });
86
117
  /**
package/dist/s3.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as StorageBackend, o as StorageService } from "./storage-uSBWFyB2.cjs";
1
+ import { o as StorageBackend, s as StorageService } from "./storage-BavPXZpA.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 { a as StorageBackend, o as StorageService } from "./storage-uSBWFyB2.mjs";
1
+ import { o as StorageBackend, s as StorageService } from "./storage-BavPXZpA.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,7 +1,7 @@
1
- import { a as StorageBackend, n as ObjectNotFound, o as StorageUnavailable, t as ObjectExists } from "./storage-BmbyYPIE.mjs";
1
+ import { a as StorageBackend, n as ObjectNotFound, o as StorageUnavailable, t as ObjectExists } from "./storage-BIKstQ72.mjs";
2
2
  import { Module, Port, Provider } from "@btravstack/di";
3
3
  import { Ok, fromPromise } from "unthrown";
4
- import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
+ import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
5
5
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
6
6
  import { Config, Env } from "@btravstack/config";
7
7
  //#region src/s3.ts
@@ -38,6 +38,14 @@ const unavailable = (operation, key, cause) => new StorageUnavailable({
38
38
  const isPreconditionFailed = (cause) => cause?.$metadata?.httpStatusCode === 412;
39
39
  /** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
40
40
  const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
41
+ /** SSE-KMS parameters when a key is named; none otherwise, so the bucket's default applies. */
42
+ const encryption = (options) => options.kmsKeyId === void 0 ? {} : {
43
+ ServerSideEncryption: "aws:kms",
44
+ SSEKMSKeyId: options.kmsKeyId
45
+ };
46
+ /** S3 reports and takes checksums in base64; the port speaks hex. */
47
+ const hexToBase64 = (hex) => Buffer.from(hex, "hex").toString("base64");
48
+ const base64ToHex = (base64) => Buffer.from(base64, "base64").toString("hex");
41
49
  /**
42
50
  * The adapter's service over a client and a bucket. A delete of a key nobody
43
51
  * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
@@ -48,14 +56,16 @@ const s3StorageBackend = (client, bucket) => ({
48
56
  Bucket: bucket,
49
57
  Key: key,
50
58
  Body: bytes,
51
- ContentType: options.contentType
59
+ ContentType: options.contentType,
60
+ ...encryption(options)
52
61
  })), (cause) => unavailable("put", key, cause)).map(() => void 0),
53
62
  putIfAbsent: (key, bytes, options) => fromPromise(client.send(new PutObjectCommand({
54
63
  Bucket: bucket,
55
64
  Key: key,
56
65
  Body: bytes,
57
66
  ContentType: options.contentType,
58
- IfNoneMatch: "*"
67
+ IfNoneMatch: "*",
68
+ ...encryption(options)
59
69
  })), (cause) => isPreconditionFailed(cause) ? new ObjectExists({ key }) : unavailable("putIfAbsent", key, cause)).map(() => void 0),
60
70
  get: (key) => fromPromise(client.send(new GetObjectCommand({
61
71
  Bucket: bucket,
@@ -64,6 +74,25 @@ const s3StorageBackend = (client, bucket) => ({
64
74
  bytes: await response.Body.transformToByteArray(),
65
75
  contentType: response.ContentType ?? "application/octet-stream"
66
76
  })), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("get", key, cause)),
77
+ copy: (from, to, options) => fromPromise((async () => client.send(new CopyObjectCommand({
78
+ Bucket: bucket,
79
+ Key: to,
80
+ CopySource: `${bucket}/${from.split("/").map(encodeURIComponent).join("/")}`,
81
+ MetadataDirective: "REPLACE",
82
+ TaggingDirective: "REPLACE",
83
+ AnnotationDirective: "EXCLUDE",
84
+ ContentType: options.contentType,
85
+ ...encryption(options)
86
+ })))(), (cause) => isMissing(cause) ? new ObjectNotFound({ key: from }) : unavailable("copy", from, cause)).map(() => void 0),
87
+ head: (key) => fromPromise(client.send(new HeadObjectCommand({
88
+ Bucket: bucket,
89
+ Key: key,
90
+ ChecksumMode: "ENABLED"
91
+ })).then((response) => ({
92
+ size: response.ContentLength ?? 0,
93
+ contentType: response.ContentType ?? "application/octet-stream",
94
+ ...response.ChecksumSHA256 === void 0 || response.ChecksumType === "COMPOSITE" ? {} : { sha256: base64ToHex(response.ChecksumSHA256) }
95
+ })), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("head", key, cause)),
67
96
  delete: (key) => fromPromise(client.send(new DeleteObjectCommand({
68
97
  Bucket: bucket,
69
98
  Key: key
@@ -76,10 +105,12 @@ const s3StorageBackend = (client, bucket) => ({
76
105
  Bucket: bucket,
77
106
  Key: key,
78
107
  ContentType: options.contentType,
79
- ContentLength: options.contentLength
108
+ ContentLength: options.contentLength,
109
+ ...options.sha256 === void 0 ? {} : { ChecksumSHA256: hexToBase64(options.sha256) }
80
110
  }), {
81
111
  expiresIn: Math.ceil(options.ttlMs / 1e3),
82
- signableHeaders: /* @__PURE__ */ new Set(["content-type"])
112
+ signableHeaders: /* @__PURE__ */ new Set(["content-type"]),
113
+ ...options.sha256 === void 0 ? {} : { unhoistableHeaders: /* @__PURE__ */ new Set(["x-amz-checksum-sha256"]) }
83
114
  }), (cause) => unavailable("presignedUpload", key, cause))
84
115
  });
85
116
  /**
@@ -1,7 +1,7 @@
1
1
  import { Port } from "@btravstack/di";
2
2
  import { 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") {};
@@ -6,7 +6,7 @@ type StoredObject = {
6
6
  readonly contentType: string;
7
7
  };
8
8
  declare const ObjectNotFound_base: import("unthrown").TaggedErrorConstructor<"ObjectNotFound">;
9
- /** The key is not there. Only `get` answers it — see `delete` and `presignedUrl` below. */
9
+ /** The key is not there. Only `get`, `head` and `copy` (its source) answer it — see `delete` and `presignedUrl` below. */
10
10
  declare class ObjectNotFound extends ObjectNotFound_base<{
11
11
  readonly key: string;
12
12
  }> {}
@@ -18,7 +18,7 @@ declare class ObjectExists extends ObjectExists_base<{
18
18
  declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
19
19
  /** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
20
20
  declare class StorageUnavailable extends StorageUnavailable_base<{
21
- readonly operation: "put" | "putIfAbsent" | "get" | "delete" | "presignedUrl" | "presignedUpload";
21
+ readonly operation: "put" | "putIfAbsent" | "get" | "copy" | "head" | "delete" | "presignedUrl" | "presignedUpload";
22
22
  readonly key: string;
23
23
  readonly reason: string;
24
24
  }> {}
@@ -27,15 +27,39 @@ declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructo
27
27
  declare class PresignNotSupported extends PresignNotSupported_base<{
28
28
  readonly key: string;
29
29
  }> {}
30
+ /** What the store knows about an object without sending its bytes. */
31
+ type ObjectHead = {
32
+ readonly size: number;
33
+ readonly contentType: string;
34
+ /** Lowercase hex of the SHA-256 the store holds for the object; absent when none was supplied at write time. */
35
+ readonly sha256?: string;
36
+ };
37
+ /**
38
+ * Written with `kmsKeyId`, an object is encrypted at rest with that KMS key
39
+ * (SSE-KMS); without it, the store's own default applies.
40
+ */
41
+ type WriteOptions = {
42
+ readonly contentType: string;
43
+ readonly kmsKeyId?: string;
44
+ };
30
45
  type StorageService = {
31
- readonly put: (key: string, bytes: Uint8Array, options: {
32
- readonly contentType: string;
33
- }) => AsyncResult<void, StorageUnavailable>;
46
+ readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, StorageUnavailable>;
34
47
  /** Writes the object only if no object holds the key: the first writer wins, atomically, in the store. */
35
- readonly putIfAbsent: (key: string, bytes: Uint8Array, options: {
36
- readonly contentType: string;
37
- }) => AsyncResult<void, ObjectExists | StorageUnavailable>;
48
+ readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | StorageUnavailable>;
38
49
  readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
50
+ /**
51
+ * Copies `from` to `to` inside the store: no bytes pass through this process.
52
+ * The copy takes `contentType` (the source's is replaced) and, with
53
+ * `kmsKeyId`, is encrypted with that key. An existing `to` is overwritten.
54
+ */
55
+ readonly copy: (from: string, to: string, options: WriteOptions) => AsyncResult<void, ObjectNotFound | StorageUnavailable>;
56
+ /**
57
+ * Size, type and checksum of an object, without reading it. `sha256` is
58
+ * present only if the store holds one: S3 keeps it for an object written with
59
+ * a SHA-256 checksum (such as through `presignedUpload` with `sha256`), and
60
+ * reports it base64-encoded, which the adapter converts to hex.
61
+ */
62
+ readonly head: (key: string) => AsyncResult<ObjectHead, ObjectNotFound | StorageUnavailable>;
39
63
  /** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
40
64
  readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
41
65
  /**
@@ -62,6 +86,11 @@ type StorageService = {
62
86
  * length would allow an unbounded single write, and there is no ceiling a
63
87
  * presigned PUT can express other than the exact number.
64
88
  *
89
+ * **`sha256` (lowercase hex, 64 characters) is optional.** When given, the
90
+ * checksum header is signed too, so the store refuses any body that does not
91
+ * hash to it, and keeps it for `head`. The client must send the same value as
92
+ * `x-amz-checksum-sha256` (base64 of the digest).
93
+ *
65
94
  * The application still decides every input: whether this caller may write
66
95
  * this key, how long the URL lives, and what size it is willing to accept.
67
96
  * The adapter computes a signature over that decision and nothing more.
@@ -70,6 +99,7 @@ type StorageService = {
70
99
  readonly ttlMs: number;
71
100
  readonly contentType: string;
72
101
  readonly contentLength: number;
102
+ readonly sha256?: string;
73
103
  }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
74
104
  };
75
105
  declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
@@ -89,4 +119,4 @@ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBa
89
119
  /** The port every adapter provides, and the one an application never depends on. */
90
120
  declare class StorageBackend extends StorageBackend_base<StorageService> {}
91
121
  //#endregion
92
- export { StorageBackend as a, StoredObject as c, Storage as i, ObjectNotFound as n, StorageService as o, PresignNotSupported as r, StorageUnavailable as s, ObjectExists as t };
122
+ export { Storage as a, StorageUnavailable as c, PresignNotSupported as i, StoredObject as l, ObjectHead as n, StorageBackend as o, ObjectNotFound as r, StorageService as s, ObjectExists as t, WriteOptions as u };
@@ -6,7 +6,7 @@ type StoredObject = {
6
6
  readonly contentType: string;
7
7
  };
8
8
  declare const ObjectNotFound_base: import("unthrown").TaggedErrorConstructor<"ObjectNotFound">;
9
- /** The key is not there. Only `get` answers it — see `delete` and `presignedUrl` below. */
9
+ /** The key is not there. Only `get`, `head` and `copy` (its source) answer it — see `delete` and `presignedUrl` below. */
10
10
  declare class ObjectNotFound extends ObjectNotFound_base<{
11
11
  readonly key: string;
12
12
  }> {}
@@ -18,7 +18,7 @@ declare class ObjectExists extends ObjectExists_base<{
18
18
  declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
19
19
  /** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
20
20
  declare class StorageUnavailable extends StorageUnavailable_base<{
21
- readonly operation: "put" | "putIfAbsent" | "get" | "delete" | "presignedUrl" | "presignedUpload";
21
+ readonly operation: "put" | "putIfAbsent" | "get" | "copy" | "head" | "delete" | "presignedUrl" | "presignedUpload";
22
22
  readonly key: string;
23
23
  readonly reason: string;
24
24
  }> {}
@@ -27,15 +27,39 @@ declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructo
27
27
  declare class PresignNotSupported extends PresignNotSupported_base<{
28
28
  readonly key: string;
29
29
  }> {}
30
+ /** What the store knows about an object without sending its bytes. */
31
+ type ObjectHead = {
32
+ readonly size: number;
33
+ readonly contentType: string;
34
+ /** Lowercase hex of the SHA-256 the store holds for the object; absent when none was supplied at write time. */
35
+ readonly sha256?: string;
36
+ };
37
+ /**
38
+ * Written with `kmsKeyId`, an object is encrypted at rest with that KMS key
39
+ * (SSE-KMS); without it, the store's own default applies.
40
+ */
41
+ type WriteOptions = {
42
+ readonly contentType: string;
43
+ readonly kmsKeyId?: string;
44
+ };
30
45
  type StorageService = {
31
- readonly put: (key: string, bytes: Uint8Array, options: {
32
- readonly contentType: string;
33
- }) => AsyncResult<void, StorageUnavailable>;
46
+ readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, StorageUnavailable>;
34
47
  /** Writes the object only if no object holds the key: the first writer wins, atomically, in the store. */
35
- readonly putIfAbsent: (key: string, bytes: Uint8Array, options: {
36
- readonly contentType: string;
37
- }) => AsyncResult<void, ObjectExists | StorageUnavailable>;
48
+ readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | StorageUnavailable>;
38
49
  readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
50
+ /**
51
+ * Copies `from` to `to` inside the store: no bytes pass through this process.
52
+ * The copy takes `contentType` (the source's is replaced) and, with
53
+ * `kmsKeyId`, is encrypted with that key. An existing `to` is overwritten.
54
+ */
55
+ readonly copy: (from: string, to: string, options: WriteOptions) => AsyncResult<void, ObjectNotFound | StorageUnavailable>;
56
+ /**
57
+ * Size, type and checksum of an object, without reading it. `sha256` is
58
+ * present only if the store holds one: S3 keeps it for an object written with
59
+ * a SHA-256 checksum (such as through `presignedUpload` with `sha256`), and
60
+ * reports it base64-encoded, which the adapter converts to hex.
61
+ */
62
+ readonly head: (key: string) => AsyncResult<ObjectHead, ObjectNotFound | StorageUnavailable>;
39
63
  /** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
40
64
  readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
41
65
  /**
@@ -62,6 +86,11 @@ type StorageService = {
62
86
  * length would allow an unbounded single write, and there is no ceiling a
63
87
  * presigned PUT can express other than the exact number.
64
88
  *
89
+ * **`sha256` (lowercase hex, 64 characters) is optional.** When given, the
90
+ * checksum header is signed too, so the store refuses any body that does not
91
+ * hash to it, and keeps it for `head`. The client must send the same value as
92
+ * `x-amz-checksum-sha256` (base64 of the digest).
93
+ *
65
94
  * The application still decides every input: whether this caller may write
66
95
  * this key, how long the URL lives, and what size it is willing to accept.
67
96
  * The adapter computes a signature over that decision and nothing more.
@@ -70,6 +99,7 @@ type StorageService = {
70
99
  readonly ttlMs: number;
71
100
  readonly contentType: string;
72
101
  readonly contentLength: number;
102
+ readonly sha256?: string;
73
103
  }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
74
104
  };
75
105
  declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
@@ -89,4 +119,4 @@ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBa
89
119
  /** The port every adapter provides, and the one an application never depends on. */
90
120
  declare class StorageBackend extends StorageBackend_base<StorageService> {}
91
121
  //#endregion
92
- export { StorageBackend as a, StoredObject as c, Storage as i, ObjectNotFound as n, StorageService as o, PresignNotSupported as r, StorageUnavailable as s, ObjectExists as t };
122
+ export { Storage as a, StorageUnavailable as c, PresignNotSupported as i, StoredObject as l, ObjectHead as n, StorageBackend as o, ObjectNotFound as r, StorageService as s, ObjectExists as t, WriteOptions as u };
@@ -1,7 +1,7 @@
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") {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/storage",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
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.37.0",
59
- "@btravstack/core": "0.37.0",
60
- "@btravstack/di": "0.37.0",
58
+ "@btravstack/config": "0.38.0",
59
+ "@btravstack/core": "0.38.0",
60
+ "@btravstack/di": "0.38.0",
61
61
  "@btravstack/internal-test-infra": "0.0.0",
62
- "@btravstack/observability": "0.37.0",
62
+ "@btravstack/observability": "0.38.0",
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.37.0",
80
- "@btravstack/core": "^0.37.0",
81
- "@btravstack/di": "^0.37.0",
79
+ "@btravstack/config": "^0.38.0",
80
+ "@btravstack/core": "^0.38.0",
81
+ "@btravstack/di": "^0.38.0",
82
82
  "unthrown": "^5.0.0"
83
83
  },
84
84
  "peerDependenciesMeta": {