@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 +12 -9
- package/dist/index.cjs +79 -30
- package/dist/index.d.cts +4 -2
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +64 -17
- package/dist/{storage-DlAQXJ1t.cjs → options-BhML_i5p.cjs} +36 -1
- package/dist/{storage-BmbyYPIE.mjs → options-DYPXkWb9.mjs} +20 -3
- package/dist/s3.cjs +87 -27
- package/dist/s3.d.cts +1 -1
- package/dist/s3.d.mts +1 -1
- package/dist/s3.mjs +84 -24
- package/dist/storage-CfN3KqWm.d.cts +154 -0
- package/dist/storage-CfN3KqWm.d.mts +154 -0
- package/package.json +8 -8
- package/dist/storage-uSBWFyB2.d.cts +0 -92
- package/dist/storage-uSBWFyB2.d.mts +0 -92
package/dist/s3.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
2
|
+
const require_options = require("./options-BhML_i5p.cjs");
|
|
3
3
|
let _btravstack_di = require("@btravstack/di");
|
|
4
4
|
let unthrown = require("unthrown");
|
|
5
5
|
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
@@ -30,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
|
|
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) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
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
|
|
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) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)(
|
|
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: [
|
|
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 {
|
|
1
|
+
import { l as StorageBackend, u as StorageService } from "./storage-CfN3KqWm.cjs";
|
|
2
2
|
import { Module, Scope } from "@btravstack/di";
|
|
3
3
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
4
|
import { ConfigInvalid, EnvReading } from "@btravstack/config";
|
package/dist/s3.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as StorageBackend, u as StorageService } from "./storage-CfN3KqWm.mjs";
|
|
2
2
|
import { Module, Scope } from "@btravstack/di";
|
|
3
3
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
4
|
import { ConfigInvalid, EnvReading } from "@btravstack/config";
|
package/dist/s3.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as StorageBackend,
|
|
1
|
+
import { a as ObjectNotFound, c as StorageBackend, i as ObjectExists, l as StorageUnavailable, n as ChecksumMismatch, t as validateOptions } from "./options-DYPXkWb9.mjs";
|
|
2
2
|
import { Module, Port, Provider } from "@btravstack/di";
|
|
3
3
|
import { Ok, fromPromise } from "unthrown";
|
|
4
|
-
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) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
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) =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 };
|