@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/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_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,41 +30,90 @@ 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 require_storage.StorageUnavailable({
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");
43
+ /** SSE-KMS parameters when a key is named; none otherwise, so the bucket's default applies. */
44
+ const encryption = (options) => options.kmsKeyId === void 0 ? {} : {
45
+ ServerSideEncryption: "aws:kms",
46
+ SSEKMSKeyId: options.kmsKeyId
47
+ };
48
+ /** S3 reports and takes checksums in base64; the port speaks hex. */
49
+ const hexToBase64 = (hex) => Buffer.from(hex, "hex").toString("base64");
50
+ const base64ToHex = (base64) => Buffer.from(base64, "base64").toString("hex");
42
51
  /**
43
52
  * The adapter's service over a client and a bucket. A delete of a key nobody
44
53
  * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
45
54
  * fiction layered over it.
46
55
  */
47
56
  const s3StorageBackend = (client, bucket) => ({
48
- put: (key, bytes, options) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.PutObjectCommand({
49
- Bucket: bucket,
50
- Key: key,
51
- Body: bytes,
52
- ContentType: options.contentType
53
- })), (cause) => unavailable("put", key, cause)).map(() => void 0),
54
- putIfAbsent: (key, bytes, options) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.PutObjectCommand({
55
- Bucket: bucket,
56
- Key: key,
57
- Body: bytes,
58
- ContentType: options.contentType,
59
- IfNoneMatch: "*"
60
- })), (cause) => isPreconditionFailed(cause) ? new require_storage.ObjectExists({ key }) : unavailable("putIfAbsent", key, cause)).map(() => void 0),
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
+ },
61
84
  get: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.GetObjectCommand({
62
85
  Bucket: bucket,
63
86
  Key: key
64
87
  })).then(async (response) => ({
65
88
  bytes: await response.Body.transformToByteArray(),
66
89
  contentType: response.ContentType ?? "application/octet-stream"
67
- })), (cause) => isMissing(cause) ? new require_storage.ObjectNotFound({ key }) : unavailable("get", key, cause)),
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
+ },
107
+ head: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.HeadObjectCommand({
108
+ Bucket: bucket,
109
+ Key: key,
110
+ ChecksumMode: "ENABLED"
111
+ })).then((response) => ({
112
+ metadata: { ...response.Metadata },
113
+ size: response.ContentLength ?? 0,
114
+ contentType: response.ContentType ?? "application/octet-stream",
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)),
68
117
  delete: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
69
118
  Bucket: bucket,
70
119
  Key: key
@@ -73,15 +122,26 @@ const s3StorageBackend = (client, bucket) => ({
73
122
  Bucket: bucket,
74
123
  Key: key
75
124
  }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause)),
76
- presignedUpload: (key, options) => (0, unthrown.fromPromise)((0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.PutObjectCommand({
77
- Bucket: bucket,
78
- Key: key,
79
- ContentType: options.contentType,
80
- ContentLength: options.contentLength
81
- }), {
82
- expiresIn: Math.ceil(options.ttlMs / 1e3),
83
- signableHeaders: /* @__PURE__ */ new Set(["content-type"])
84
- }), (cause) => unavailable("presignedUpload", key, cause))
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
+ }
85
145
  });
86
146
  /**
87
147
  * The S3 adapter: one client, built with the scope and destroyed with it, and
@@ -111,12 +171,12 @@ const s3Storage = () => (0, _btravstack_di.Module)("S3Storage")({
111
171
  client.destroy();
112
172
  }
113
173
  }),
114
- (0, _btravstack_di.Provider)(require_storage.StorageBackend)({
174
+ (0, _btravstack_di.Provider)(require_options.StorageBackend)({
115
175
  inject: { connection: S3Connection },
116
176
  sync: ({ connection }) => s3StorageBackend(connection.client, connection.bucket)
117
177
  })
118
178
  ],
119
- exports: [require_storage.StorageBackend]
179
+ exports: [require_options.StorageBackend]
120
180
  });
121
181
  //#endregion
122
182
  exports.s3Storage = s3Storage;
package/dist/s3.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as StorageBackend, o as StorageService } from "./storage-uSBWFyB2.cjs";
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 { a as StorageBackend, o as StorageService } from "./storage-uSBWFyB2.mjs";
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,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 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
- 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
@@ -36,27 +36,50 @@ 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");
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");
41
50
  /**
42
51
  * The adapter's service over a client and a bucket. A delete of a key nobody
43
52
  * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
44
53
  * fiction layered over it.
45
54
  */
46
55
  const s3StorageBackend = (client, bucket) => ({
47
- put: (key, bytes, options) => fromPromise(client.send(new PutObjectCommand({
48
- Bucket: bucket,
49
- Key: key,
50
- Body: bytes,
51
- ContentType: options.contentType
52
- })), (cause) => unavailable("put", key, cause)).map(() => void 0),
53
- putIfAbsent: (key, bytes, options) => fromPromise(client.send(new PutObjectCommand({
54
- Bucket: bucket,
55
- Key: key,
56
- Body: bytes,
57
- ContentType: options.contentType,
58
- IfNoneMatch: "*"
59
- })), (cause) => isPreconditionFailed(cause) ? new ObjectExists({ key }) : unavailable("putIfAbsent", key, cause)).map(() => void 0),
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
+ },
60
83
  get: (key) => fromPromise(client.send(new GetObjectCommand({
61
84
  Bucket: bucket,
62
85
  Key: key
@@ -64,6 +87,32 @@ const s3StorageBackend = (client, bucket) => ({
64
87
  bytes: await response.Body.transformToByteArray(),
65
88
  contentType: response.ContentType ?? "application/octet-stream"
66
89
  })), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("get", key, cause)),
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
+ },
106
+ head: (key) => fromPromise(client.send(new HeadObjectCommand({
107
+ Bucket: bucket,
108
+ Key: key,
109
+ ChecksumMode: "ENABLED"
110
+ })).then((response) => ({
111
+ metadata: { ...response.Metadata },
112
+ size: response.ContentLength ?? 0,
113
+ contentType: response.ContentType ?? "application/octet-stream",
114
+ ...response.ChecksumSHA256 === void 0 || response.ChecksumType === "COMPOSITE" || !/^[A-Za-z0-9+/]{43}=$/.test(response.ChecksumSHA256) ? {} : { sha256: base64ToHex(response.ChecksumSHA256) }
115
+ })), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("head", key, cause)),
67
116
  delete: (key) => fromPromise(client.send(new DeleteObjectCommand({
68
117
  Bucket: bucket,
69
118
  Key: key
@@ -72,15 +121,26 @@ const s3StorageBackend = (client, bucket) => ({
72
121
  Bucket: bucket,
73
122
  Key: key
74
123
  }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause)),
75
- presignedUpload: (key, options) => fromPromise(getSignedUrl(client, new PutObjectCommand({
76
- Bucket: bucket,
77
- Key: key,
78
- ContentType: options.contentType,
79
- ContentLength: options.contentLength
80
- }), {
81
- expiresIn: Math.ceil(options.ttlMs / 1e3),
82
- signableHeaders: /* @__PURE__ */ new Set(["content-type"])
83
- }), (cause) => unavailable("presignedUpload", key, cause))
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
+ }
84
144
  });
85
145
  /**
86
146
  * The S3 adapter: one client, built with the scope and destroyed with it, and
@@ -0,0 +1,154 @@
1
+ import { AsyncResult } from "unthrown";
2
+ //#region src/storage.d.ts
3
+ /** One stored object: its bytes and what they are. */
4
+ type StoredObject = {
5
+ readonly bytes: Uint8Array;
6
+ readonly contentType: string;
7
+ };
8
+ declare const ObjectNotFound_base: import("unthrown").TaggedErrorConstructor<"ObjectNotFound">;
9
+ /** The key is not there. Only `get`, `head` and `copy` (its source) answer it — see `delete` and `presignedUrl` below. */
10
+ declare class ObjectNotFound extends ObjectNotFound_base<{
11
+ readonly key: string;
12
+ }> {}
13
+ declare const ObjectExists_base: import("unthrown").TaggedErrorConstructor<"ObjectExists">;
14
+ /** An object already holds the key. Only `putIfAbsent` answers it. */
15
+ declare class ObjectExists extends ObjectExists_base<{
16
+ readonly key: string;
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
+ }> {}
29
+ declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
30
+ /** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
31
+ declare class StorageUnavailable extends StorageUnavailable_base<{
32
+ readonly operation: "put" | "putIfAbsent" | "get" | "copy" | "head" | "delete" | "presignedUrl" | "presignedUpload";
33
+ readonly key: string;
34
+ readonly reason: string;
35
+ }> {}
36
+ declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructor<"PresignNotSupported">;
37
+ /** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
38
+ declare class PresignNotSupported extends PresignNotSupported_base<{
39
+ readonly key: string;
40
+ }> {}
41
+ /** What the store knows about an object without sending its bytes. */
42
+ type ObjectHead = {
43
+ readonly size: number;
44
+ /** An owned snapshot of user metadata; empty when none was written. */
45
+ readonly metadata: Readonly<Record<string, string>>;
46
+ readonly contentType: string;
47
+ /** Lowercase hex of a full-object SHA-256; absent when the store holds none. Composite multipart checksums are excluded. */
48
+ readonly sha256?: string;
49
+ };
50
+ /**
51
+ * Written with `kmsKeyId`, an object is encrypted at rest with that KMS key
52
+ * (SSE-KMS); without it, the store's own default applies.
53
+ */
54
+ type WriteOptions = {
55
+ readonly contentType: string;
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;
70
+ };
71
+ type StorageService = {
72
+ readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
73
+ /** Writes the object only if no object holds the key: the first writer wins, atomically, in the store. */
74
+ readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
75
+ readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
76
+ /**
77
+ * Copies `from` to `to` inside the store: no bytes pass through this process.
78
+ * The copy replaces metadata (empty when omitted) and `contentType`, and, with
79
+ * `kmsKeyId`, is encrypted with that key. An existing `to` is overwritten.
80
+ */
81
+ readonly copy: (from: string, to: string, options: CopyOptions) => AsyncResult<void, ObjectNotFound | InvalidStorageOptions | StorageUnavailable>;
82
+ /**
83
+ * Size, type and checksum of an object, without reading it. `sha256` is
84
+ * present only if the store holds one: S3 keeps it for an object written with
85
+ * a SHA-256 checksum (write/presign `sha256`, or copy `checksum`), and
86
+ * reports it base64-encoded, which the adapter converts to hex.
87
+ */
88
+ readonly head: (key: string) => AsyncResult<ObjectHead, ObjectNotFound | StorageUnavailable>;
89
+ /** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
90
+ readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
91
+ /**
92
+ * A URL that reads the object for `ttlMs`, without the caller holding
93
+ * credentials.
94
+ *
95
+ * There is **no `ObjectNotFound` arm**: presigning is a signature computation
96
+ * that asks the store nothing, so a URL for an absent key is minted happily
97
+ * and 404s when followed. Checking would cost a HEAD per call.
98
+ */
99
+ readonly presignedUrl: (key: string, options: {
100
+ readonly ttlMs: number;
101
+ }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
102
+ /**
103
+ * A URL that writes the object for `ttlMs`, without the caller holding
104
+ * credentials — so the bytes go from the client to the store and never
105
+ * through this process.
106
+ *
107
+ * **`contentType` and `contentLength` are signed, not advisory.** They are
108
+ * part of the signature, so a client sending different ones is refused by the
109
+ * store. The same URL can write the key repeatedly until expiry, replacing
110
+ * the previous object with different bytes of that size and type. Neither
111
+ * signed header proves the bytes. `contentLength` is required — an unsigned
112
+ * length would allow an unbounded single write, and there is no ceiling a
113
+ * presigned PUT can express other than the exact number.
114
+ *
115
+ * **`sha256` (lowercase hex, 64 characters) is optional.** When given, the
116
+ * checksum header is signed too, so the store refuses any body that does not
117
+ * hash to it, and keeps it for `head`. The client must send the same value as
118
+ * `x-amz-checksum-sha256` (base64 of the digest).
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
+ *
125
+ * The application still decides every input: whether this caller may write
126
+ * this key, how long the URL lives, and what size it is willing to accept.
127
+ * The adapter computes a signature over that decision and nothing more.
128
+ */
129
+ readonly presignedUpload: (key: string, options: {
130
+ readonly ttlMs: number;
131
+ readonly contentType: string;
132
+ readonly contentLength: number;
133
+ readonly sha256?: string;
134
+ readonly kmsKeyId?: string;
135
+ }) => AsyncResult<string, PresignNotSupported | InvalidStorageOptions | StorageUnavailable>;
136
+ };
137
+ declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
138
+ /**
139
+ * The port an application depends on.
140
+ *
141
+ * Keys are plain strings the caller composes, tenant included: a store is an
142
+ * application service, and the framework has no concept of a tenant to put in a
143
+ * slot.
144
+ *
145
+ * **Bytes, not streams.** An object here is a document. Streaming would change
146
+ * every signature, adapter and test to serve a case that wants a different
147
+ * design anyway — a stated non-goal, not an oversight.
148
+ */
149
+ declare class Storage extends Storage_base<StorageService> {}
150
+ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBackend">;
151
+ /** The port every adapter provides, and the one an application never depends on. */
152
+ declare class StorageBackend extends StorageBackend_base<StorageService> {}
153
+ //#endregion
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 };
@@ -0,0 +1,154 @@
1
+ import { AsyncResult } from "unthrown";
2
+ //#region src/storage.d.ts
3
+ /** One stored object: its bytes and what they are. */
4
+ type StoredObject = {
5
+ readonly bytes: Uint8Array;
6
+ readonly contentType: string;
7
+ };
8
+ declare const ObjectNotFound_base: import("unthrown").TaggedErrorConstructor<"ObjectNotFound">;
9
+ /** The key is not there. Only `get`, `head` and `copy` (its source) answer it — see `delete` and `presignedUrl` below. */
10
+ declare class ObjectNotFound extends ObjectNotFound_base<{
11
+ readonly key: string;
12
+ }> {}
13
+ declare const ObjectExists_base: import("unthrown").TaggedErrorConstructor<"ObjectExists">;
14
+ /** An object already holds the key. Only `putIfAbsent` answers it. */
15
+ declare class ObjectExists extends ObjectExists_base<{
16
+ readonly key: string;
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
+ }> {}
29
+ declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
30
+ /** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
31
+ declare class StorageUnavailable extends StorageUnavailable_base<{
32
+ readonly operation: "put" | "putIfAbsent" | "get" | "copy" | "head" | "delete" | "presignedUrl" | "presignedUpload";
33
+ readonly key: string;
34
+ readonly reason: string;
35
+ }> {}
36
+ declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructor<"PresignNotSupported">;
37
+ /** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
38
+ declare class PresignNotSupported extends PresignNotSupported_base<{
39
+ readonly key: string;
40
+ }> {}
41
+ /** What the store knows about an object without sending its bytes. */
42
+ type ObjectHead = {
43
+ readonly size: number;
44
+ /** An owned snapshot of user metadata; empty when none was written. */
45
+ readonly metadata: Readonly<Record<string, string>>;
46
+ readonly contentType: string;
47
+ /** Lowercase hex of a full-object SHA-256; absent when the store holds none. Composite multipart checksums are excluded. */
48
+ readonly sha256?: string;
49
+ };
50
+ /**
51
+ * Written with `kmsKeyId`, an object is encrypted at rest with that KMS key
52
+ * (SSE-KMS); without it, the store's own default applies.
53
+ */
54
+ type WriteOptions = {
55
+ readonly contentType: string;
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;
70
+ };
71
+ type StorageService = {
72
+ readonly put: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
73
+ /** Writes the object only if no object holds the key: the first writer wins, atomically, in the store. */
74
+ readonly putIfAbsent: (key: string, bytes: Uint8Array, options: WriteOptions) => AsyncResult<void, ObjectExists | InvalidStorageOptions | ChecksumMismatch | StorageUnavailable>;
75
+ readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
76
+ /**
77
+ * Copies `from` to `to` inside the store: no bytes pass through this process.
78
+ * The copy replaces metadata (empty when omitted) and `contentType`, and, with
79
+ * `kmsKeyId`, is encrypted with that key. An existing `to` is overwritten.
80
+ */
81
+ readonly copy: (from: string, to: string, options: CopyOptions) => AsyncResult<void, ObjectNotFound | InvalidStorageOptions | StorageUnavailable>;
82
+ /**
83
+ * Size, type and checksum of an object, without reading it. `sha256` is
84
+ * present only if the store holds one: S3 keeps it for an object written with
85
+ * a SHA-256 checksum (write/presign `sha256`, or copy `checksum`), and
86
+ * reports it base64-encoded, which the adapter converts to hex.
87
+ */
88
+ readonly head: (key: string) => AsyncResult<ObjectHead, ObjectNotFound | StorageUnavailable>;
89
+ /** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
90
+ readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
91
+ /**
92
+ * A URL that reads the object for `ttlMs`, without the caller holding
93
+ * credentials.
94
+ *
95
+ * There is **no `ObjectNotFound` arm**: presigning is a signature computation
96
+ * that asks the store nothing, so a URL for an absent key is minted happily
97
+ * and 404s when followed. Checking would cost a HEAD per call.
98
+ */
99
+ readonly presignedUrl: (key: string, options: {
100
+ readonly ttlMs: number;
101
+ }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
102
+ /**
103
+ * A URL that writes the object for `ttlMs`, without the caller holding
104
+ * credentials — so the bytes go from the client to the store and never
105
+ * through this process.
106
+ *
107
+ * **`contentType` and `contentLength` are signed, not advisory.** They are
108
+ * part of the signature, so a client sending different ones is refused by the
109
+ * store. The same URL can write the key repeatedly until expiry, replacing
110
+ * the previous object with different bytes of that size and type. Neither
111
+ * signed header proves the bytes. `contentLength` is required — an unsigned
112
+ * length would allow an unbounded single write, and there is no ceiling a
113
+ * presigned PUT can express other than the exact number.
114
+ *
115
+ * **`sha256` (lowercase hex, 64 characters) is optional.** When given, the
116
+ * checksum header is signed too, so the store refuses any body that does not
117
+ * hash to it, and keeps it for `head`. The client must send the same value as
118
+ * `x-amz-checksum-sha256` (base64 of the digest).
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
+ *
125
+ * The application still decides every input: whether this caller may write
126
+ * this key, how long the URL lives, and what size it is willing to accept.
127
+ * The adapter computes a signature over that decision and nothing more.
128
+ */
129
+ readonly presignedUpload: (key: string, options: {
130
+ readonly ttlMs: number;
131
+ readonly contentType: string;
132
+ readonly contentLength: number;
133
+ readonly sha256?: string;
134
+ readonly kmsKeyId?: string;
135
+ }) => AsyncResult<string, PresignNotSupported | InvalidStorageOptions | StorageUnavailable>;
136
+ };
137
+ declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
138
+ /**
139
+ * The port an application depends on.
140
+ *
141
+ * Keys are plain strings the caller composes, tenant included: a store is an
142
+ * application service, and the framework has no concept of a tenant to put in a
143
+ * slot.
144
+ *
145
+ * **Bytes, not streams.** An object here is a document. Streaming would change
146
+ * every signature, adapter and test to serve a case that wants a different
147
+ * design anyway — a stated non-goal, not an oversight.
148
+ */
149
+ declare class Storage extends Storage_base<StorageService> {}
150
+ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBackend">;
151
+ /** The port every adapter provides, and the one an application never depends on. */
152
+ declare class StorageBackend extends StorageBackend_base<StorageService> {}
153
+ //#endregion
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 };