@btravstack/storage 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benoit TRAVERS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @btravstack/storage
2
+
3
+ > The object-storage port for [`@btravstack/core`](../core): one `Storage` an
4
+ > application depends on, an in-memory adapter, an S3-compatible one with
5
+ > presigned reads, and a span, a count and a log line per operation.
6
+
7
+ ๐Ÿ“– **[Documentation](https://btravstack.github.io/btravstack/reference/storage)** ยท
8
+ [API Reference](https://btravstack.github.io/btravstack/api/storage/)
9
+
10
+ ```sh
11
+ pnpm add @btravstack/storage @btravstack/core @btravstack/config @btravstack/di unthrown
12
+ ```
13
+
14
+ Four peer dependencies, plus `@aws-sdk/client-s3` and
15
+ `@aws-sdk/s3-request-presigner` โ€” optional, and needed only if you compose the
16
+ S3 adapter. Instrumentation needs no peer of its own: the `Logger`, `Tracer`
17
+ and `Meter` it depends on are
18
+ [the kernel's ports](https://btravstack.github.io/btravstack/reference/core/observability).
19
+ Node `>=20`.
20
+
21
+ ## A worked example
22
+
23
+ <!-- doctest: group=order-temporal-worker -->
24
+ <!-- doctest: prelude
25
+ import { Module } from "@btravstack/di";
26
+ import { Logger } from "@btravstack/core";
27
+ import { observability } from "@btravstack/observability";
28
+ import { otel } from "@btravstack/observability/otel";
29
+ import { P } from "unthrown";
30
+ -->
31
+
32
+ Storing a document, and reading it back through a URL that carries no
33
+ credentials:
34
+
35
+ ```ts
36
+ import {
37
+ Port as InvoicePort,
38
+ Provider as InvoiceProvider,
39
+ } from "@btravstack/di";
40
+ import { Storage } from "@btravstack/storage";
41
+ import type { AsyncResult } from "unthrown";
42
+
43
+ class Invoices extends InvoicePort("ReadmeInvoices")<{
44
+ readonly store: (
45
+ tenantId: string,
46
+ id: string,
47
+ pdf: Uint8Array,
48
+ ) => AsyncResult<void, never>;
49
+ readonly link: (
50
+ tenantId: string,
51
+ id: string,
52
+ ) => AsyncResult<string | undefined, never>;
53
+ }> {}
54
+
55
+ // The tenant is in the key because the port has no slot for one โ€” the same
56
+ // rule a cache key follows.
57
+ const keyFor = (tenantId: string, id: string) =>
58
+ `invoices/${tenantId}/${id}.pdf`;
59
+
60
+ export const invoices = InvoiceProvider(Invoices)(
61
+ { storage: Storage },
62
+ {
63
+ sync: ({ storage }) => ({
64
+ store: (tenantId, id, pdf) =>
65
+ storage
66
+ .put(keyFor(tenantId, id), pdf, { contentType: "application/pdf" })
67
+ // A document that failed to store is YOUR decision to make; here it
68
+ // is swallowed because the instrumented store already logged and
69
+ // counted it one layer down.
70
+ .recoverErrCases((matcher) =>
71
+ matcher.with(P.tag("StorageUnavailable"), () => undefined),
72
+ ),
73
+ // Serving bytes through your own process is the anti-pattern this arm
74
+ // exists to avoid: the caller fetches the object directly, for a minute.
75
+ link: (tenantId, id) =>
76
+ storage
77
+ .presignedUrl(keyFor(tenantId, id), { ttlMs: 60_000 })
78
+ .recoverErrCases((matcher) =>
79
+ matcher
80
+ .with(P.tag("StorageUnavailable"), () => undefined)
81
+ .with(P.tag("PresignNotSupported"), () => undefined),
82
+ ),
83
+ }),
84
+ },
85
+ );
86
+ ```
87
+
88
+ The composition โ€” the adapter, and whether operations are instrumented,
89
+ decided here and nowhere else:
90
+
91
+ ```ts
92
+ import { storage } from "@btravstack/storage";
93
+ import { s3Storage } from "@btravstack/storage/s3";
94
+
95
+ export const InvoicesApp = Module("InvoicesApp")({
96
+ imports: [
97
+ // Instrumented by default, which is why `observability()` and `otel()`
98
+ // are below. Pass `instrumented: false` for the same graph without them.
99
+ storage({ adapter: s3Storage() }),
100
+ observability(),
101
+ otel(),
102
+ ],
103
+ provides: [invoices],
104
+ exports: [Invoices, Logger],
105
+ });
106
+ ```
107
+
108
+ ## Options
109
+
110
+ | Option | Where | What it is |
111
+ | ----------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------- |
112
+ | `adapter` | `storage({ adapter })` | the adapter module providing `StorageBackend` โ€” required |
113
+ | `instrumented` | `storage({ instrumented })` | span, count and log every operation (default `true`; `false` opts out) |
114
+ | `STORAGE_S3_ENDPOINT` | environment, read by `s3Storage()` | the store's endpoint โ€” required |
115
+ | `STORAGE_S3_BUCKET` | environment | the bucket โ€” required |
116
+ | `STORAGE_S3_ACCESS_KEY_ID` / `STORAGE_S3_SECRET_ACCESS_KEY` | environment | the credentials โ€” required |
117
+ | `STORAGE_S3_REGION` | environment | default `us-east-1` |
118
+
119
+ The full table โ€” defaults, semantics and the reasoning โ€” lives on
120
+ [the reference page](https://btravstack.github.io/btravstack/reference/storage),
121
+ which is this list's one detailed home.
122
+
123
+ ## What it decides, and what it does not
124
+
125
+ **It decides** that a missing object is an ordinary answer rather than a fault
126
+ (counted `not_found`, logged at `info`), that `delete` is idempotent, that
127
+ presigning asks the store nothing โ€” and that an adapter which cannot presign
128
+ says so instead of minting a URL that would fail only in production.
129
+
130
+ **It does not decide** what your keys look like, whether a failed write should
131
+ fail your request, or how long a link should live. There is no streaming, no
132
+ presigned writes, no listing and no bucket management; the reasons are in
133
+ [`CLAUDE.md`](./CLAUDE.md).
package/dist/index.cjs ADDED
@@ -0,0 +1,144 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_storage = require("./storage-QVPXOcO6.cjs");
3
+ let _btravstack_di = require("@btravstack/di");
4
+ let unthrown = require("unthrown");
5
+ let _btravstack_core = require("@btravstack/core");
6
+ //#region src/memory.ts
7
+ /**
8
+ * The in-process adapter: a `Map`, and an honest refusal to presign.
9
+ *
10
+ * `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
11
+ * which would be the worst kind of double โ€” one that passes locally and fails in
12
+ * the deployment for a reason no test could have shown.
13
+ *
14
+ * ponytail: no size limit, so a process storing unbounded objects grows
15
+ * unbounded. The upgrade path is the S3 adapter.
16
+ */
17
+ const memoryStorageBackend = () => {
18
+ const objects = /* @__PURE__ */ new Map();
19
+ return {
20
+ put: (key, bytes, options) => {
21
+ objects.set(key, {
22
+ bytes,
23
+ contentType: options.contentType
24
+ });
25
+ return (0, unthrown.OkAsync)();
26
+ },
27
+ get: (key) => {
28
+ const object = objects.get(key);
29
+ return object === void 0 ? (0, unthrown.ErrAsync)(new require_storage.ObjectNotFound({ key })) : (0, unthrown.OkAsync)(object);
30
+ },
31
+ delete: (key) => {
32
+ objects.delete(key);
33
+ return (0, unthrown.OkAsync)();
34
+ },
35
+ presignedUrl: (key) => (0, unthrown.ErrAsync)(new require_storage.PresignNotSupported({ key }))
36
+ };
37
+ };
38
+ /** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
39
+ const memoryStorageProvider = () => (0, _btravstack_di.Provider)(require_storage.StorageBackend)({}, { sync: () => memoryStorageBackend() });
40
+ /** The adapter as a module, which is the shape `storage({ adapter })` takes. */
41
+ const memoryStorage = () => (0, _btravstack_di.Module)("MemoryStorage")({
42
+ provides: [memoryStorageProvider()],
43
+ exports: [require_storage.StorageBackend]
44
+ });
45
+ //#endregion
46
+ //#region src/instrument.ts
47
+ /**
48
+ * One operation, wrapped: a span around it, a count of how it came out, and a
49
+ * log line if it failed.
50
+ *
51
+ * **A missing object is counted `not_found` and logged at `info`, not
52
+ * `error`.** Asking for something that is not there is an ordinary answer, and
53
+ * a dashboard that treats it as a fault teaches its readers to ignore the fault
54
+ * line. `StorageUnavailable` is what pages somebody.
55
+ */
56
+ const observed = (call, { operation, key, logger, tracer, operations }) => {
57
+ const span = tracer.startSpan(`storage.${operation}`);
58
+ span.setAttributes({ "btravstack.storage.key": key });
59
+ return call().tap(() => {
60
+ operations.add(1, {
61
+ operation,
62
+ outcome: "ok"
63
+ });
64
+ span.end();
65
+ }).tapFailure((failure) => {
66
+ const cause = failure.tag === "Err" ? failure.error : failure.cause;
67
+ const message = failure.tag !== "Err" ? void 0 : failure.error._tag === "ObjectNotFound" ? "the object was not there" : failure.error._tag === "PresignNotSupported" ? "this store cannot mint a url" : void 0;
68
+ operations.add(1, {
69
+ operation,
70
+ outcome: message === void 0 ? "error" : "not_found"
71
+ });
72
+ if (message === void 0) {
73
+ logger.error("the store could not answer", {
74
+ operation,
75
+ key
76
+ }, cause);
77
+ span.setStatus({ code: _btravstack_core.SPAN_STATUS.error });
78
+ } else logger.info(message, {
79
+ operation,
80
+ key
81
+ });
82
+ span.end();
83
+ });
84
+ };
85
+ const instrument = (backend, logger, tracer, meter) => {
86
+ const operations = meter.createCounter("btravstack.storage.operations", { description: "Storage operations, by operation and outcome" });
87
+ const context = (operation, key) => ({
88
+ operation,
89
+ key,
90
+ logger,
91
+ tracer,
92
+ operations
93
+ });
94
+ return {
95
+ put: (key, bytes, options) => observed(() => backend.put(key, bytes, options), context("put", key)),
96
+ get: (key) => observed(() => backend.get(key), context("get", key)),
97
+ delete: (key) => observed(() => backend.delete(key), context("delete", key)),
98
+ presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key))
99
+ };
100
+ };
101
+ //#endregion
102
+ //#region src/module.ts
103
+ /**
104
+ * The storage starter: an adapter, and `Storage` provided from it โ€”
105
+ * instrumented or not, decided here at the composition root.
106
+ *
107
+ * ```ts
108
+ * storage({ adapter: s3Storage() }); // spans, counts, log lines
109
+ * storage({ adapter: s3Storage(), instrumented: false }); // just a store
110
+ * ```
111
+ *
112
+ * **Two ports, because di allows one provider per port per graph**: the port an
113
+ * application depends on must not be the one an adapter provides, which is also
114
+ * why instrumentation is a flag on the composition rather than a wrapper.
115
+ */
116
+ const storage = ({ adapter, instrumented }) => instrumented !== false ? (0, _btravstack_di.Module)("InstrumentedStorage")({
117
+ needs: [
118
+ _btravstack_core.Logger,
119
+ _btravstack_core.Meter,
120
+ _btravstack_core.Tracer
121
+ ],
122
+ imports: [adapter],
123
+ provides: [(0, _btravstack_di.Provider)(require_storage.Storage)({
124
+ backend: require_storage.StorageBackend,
125
+ logger: _btravstack_core.Logger,
126
+ tracer: _btravstack_core.Tracer,
127
+ meter: _btravstack_core.Meter
128
+ }, { sync: ({ backend, logger, tracer, meter }) => instrument(backend, logger, tracer, meter) })],
129
+ exports: [require_storage.Storage]
130
+ }) : (0, _btravstack_di.Module)("Storage")({
131
+ imports: [adapter],
132
+ provides: [(0, _btravstack_di.Provider)(require_storage.Storage)({ backend: require_storage.StorageBackend }, { sync: ({ backend }) => backend })],
133
+ exports: [require_storage.Storage]
134
+ });
135
+ //#endregion
136
+ exports.ObjectNotFound = require_storage.ObjectNotFound;
137
+ exports.PresignNotSupported = require_storage.PresignNotSupported;
138
+ exports.Storage = require_storage.Storage;
139
+ exports.StorageBackend = require_storage.StorageBackend;
140
+ exports.StorageUnavailable = require_storage.StorageUnavailable;
141
+ exports.memoryStorage = memoryStorage;
142
+ exports.memoryStorageBackend = memoryStorageBackend;
143
+ exports.memoryStorageProvider = memoryStorageProvider;
144
+ exports.storage = storage;
@@ -0,0 +1,56 @@
1
+ import { a as StorageService, i as StorageBackend, n as PresignNotSupported, o as StorageUnavailable, r as Storage, s as StoredObject, t as ObjectNotFound } from "./storage-C9j3zj7r.cjs";
2
+ import { Module, Provider } from "@btravstack/di";
3
+ import { Logger, Meter, Tracer } from "@btravstack/core";
4
+ //#region src/memory.d.ts
5
+ /**
6
+ * The in-process adapter: a `Map`, and an honest refusal to presign.
7
+ *
8
+ * `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
9
+ * which would be the worst kind of double โ€” one that passes locally and fails in
10
+ * the deployment for a reason no test could have shown.
11
+ *
12
+ * ponytail: no size limit, so a process storing unbounded objects grows
13
+ * unbounded. The upgrade path is the S3 adapter.
14
+ */
15
+ declare const memoryStorageBackend: () => StorageService;
16
+ /** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
17
+ declare const memoryStorageProvider: () => Provider<StorageBackend, never, never> & {
18
+ readonly port: typeof StorageBackend;
19
+ };
20
+ /** The adapter as a module, which is the shape `storage({ adapter })` takes. */
21
+ declare const memoryStorage: () => Module<StorageBackend, never, never>;
22
+ //#endregion
23
+ //#region src/module.d.ts
24
+ type StorageOptions<E, N, Instrumented extends boolean> = {
25
+ /**
26
+ * The adapter module: `memoryStorage()` from this entry point,
27
+ * `s3Storage()` from `@btravstack/storage/s3`, or one an application
28
+ * wrote itself over `StorageBackend`.
29
+ */
30
+ readonly adapter: Module<StorageBackend, E, N>;
31
+ /**
32
+ * Span, count and log every operation. **Default `true`**, `false` opts out.
33
+ *
34
+ * On by default because telemetry that is missing is discovered during an
35
+ * incident. The cost is stated rather than hidden: instrumenting puts
36
+ * `Logger`, `Meter` and `Tracer` in this module's `Needs`, so a root without
37
+ * `observability()` and `otel()` gets a compile error naming all three.
38
+ */
39
+ readonly instrumented?: Instrumented;
40
+ };
41
+ /**
42
+ * The storage starter: an adapter, and `Storage` provided from it โ€”
43
+ * instrumented or not, decided here at the composition root.
44
+ *
45
+ * ```ts
46
+ * storage({ adapter: s3Storage() }); // spans, counts, log lines
47
+ * storage({ adapter: s3Storage(), instrumented: false }); // just a store
48
+ * ```
49
+ *
50
+ * **Two ports, because di allows one provider per port per graph**: the port an
51
+ * application depends on must not be the one an adapter provides, which is also
52
+ * why instrumentation is a flag on the composition rather than a wrapper.
53
+ */
54
+ declare const storage: <E, N, Instrumented extends boolean = true>({ adapter, instrumented }: StorageOptions<E, N, Instrumented>) => Module<Storage, E, Instrumented extends true ? N | Logger | Meter | Tracer : N>;
55
+ //#endregion
56
+ export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
@@ -0,0 +1,56 @@
1
+ import { a as StorageService, i as StorageBackend, n as PresignNotSupported, o as StorageUnavailable, r as Storage, s as StoredObject, t as ObjectNotFound } from "./storage-C9j3zj7r.mjs";
2
+ import { Module, Provider } from "@btravstack/di";
3
+ import { Logger, Meter, Tracer } from "@btravstack/core";
4
+ //#region src/memory.d.ts
5
+ /**
6
+ * The in-process adapter: a `Map`, and an honest refusal to presign.
7
+ *
8
+ * `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
9
+ * which would be the worst kind of double โ€” one that passes locally and fails in
10
+ * the deployment for a reason no test could have shown.
11
+ *
12
+ * ponytail: no size limit, so a process storing unbounded objects grows
13
+ * unbounded. The upgrade path is the S3 adapter.
14
+ */
15
+ declare const memoryStorageBackend: () => StorageService;
16
+ /** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
17
+ declare const memoryStorageProvider: () => Provider<StorageBackend, never, never> & {
18
+ readonly port: typeof StorageBackend;
19
+ };
20
+ /** The adapter as a module, which is the shape `storage({ adapter })` takes. */
21
+ declare const memoryStorage: () => Module<StorageBackend, never, never>;
22
+ //#endregion
23
+ //#region src/module.d.ts
24
+ type StorageOptions<E, N, Instrumented extends boolean> = {
25
+ /**
26
+ * The adapter module: `memoryStorage()` from this entry point,
27
+ * `s3Storage()` from `@btravstack/storage/s3`, or one an application
28
+ * wrote itself over `StorageBackend`.
29
+ */
30
+ readonly adapter: Module<StorageBackend, E, N>;
31
+ /**
32
+ * Span, count and log every operation. **Default `true`**, `false` opts out.
33
+ *
34
+ * On by default because telemetry that is missing is discovered during an
35
+ * incident. The cost is stated rather than hidden: instrumenting puts
36
+ * `Logger`, `Meter` and `Tracer` in this module's `Needs`, so a root without
37
+ * `observability()` and `otel()` gets a compile error naming all three.
38
+ */
39
+ readonly instrumented?: Instrumented;
40
+ };
41
+ /**
42
+ * The storage starter: an adapter, and `Storage` provided from it โ€”
43
+ * instrumented or not, decided here at the composition root.
44
+ *
45
+ * ```ts
46
+ * storage({ adapter: s3Storage() }); // spans, counts, log lines
47
+ * storage({ adapter: s3Storage(), instrumented: false }); // just a store
48
+ * ```
49
+ *
50
+ * **Two ports, because di allows one provider per port per graph**: the port an
51
+ * application depends on must not be the one an adapter provides, which is also
52
+ * why instrumentation is a flag on the composition rather than a wrapper.
53
+ */
54
+ declare const storage: <E, N, Instrumented extends boolean = true>({ adapter, instrumented }: StorageOptions<E, N, Instrumented>) => Module<Storage, E, Instrumented extends true ? N | Logger | Meter | Tracer : N>;
55
+ //#endregion
56
+ export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
package/dist/index.mjs ADDED
@@ -0,0 +1,135 @@
1
+ import { a as StorageUnavailable, i as StorageBackend, n as PresignNotSupported, r as Storage, t as ObjectNotFound } from "./storage-CRkwIhac.mjs";
2
+ import { Module, Provider } from "@btravstack/di";
3
+ import { ErrAsync, OkAsync } from "unthrown";
4
+ import { Logger, Meter, SPAN_STATUS, Tracer } from "@btravstack/core";
5
+ //#region src/memory.ts
6
+ /**
7
+ * The in-process adapter: a `Map`, and an honest refusal to presign.
8
+ *
9
+ * `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
10
+ * which would be the worst kind of double โ€” one that passes locally and fails in
11
+ * the deployment for a reason no test could have shown.
12
+ *
13
+ * ponytail: no size limit, so a process storing unbounded objects grows
14
+ * unbounded. The upgrade path is the S3 adapter.
15
+ */
16
+ const memoryStorageBackend = () => {
17
+ const objects = /* @__PURE__ */ new Map();
18
+ return {
19
+ put: (key, bytes, options) => {
20
+ objects.set(key, {
21
+ bytes,
22
+ contentType: options.contentType
23
+ });
24
+ return OkAsync();
25
+ },
26
+ get: (key) => {
27
+ const object = objects.get(key);
28
+ return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync(object);
29
+ },
30
+ delete: (key) => {
31
+ objects.delete(key);
32
+ return OkAsync();
33
+ },
34
+ presignedUrl: (key) => ErrAsync(new PresignNotSupported({ key }))
35
+ };
36
+ };
37
+ /** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
38
+ const memoryStorageProvider = () => Provider(StorageBackend)({}, { sync: () => memoryStorageBackend() });
39
+ /** The adapter as a module, which is the shape `storage({ adapter })` takes. */
40
+ const memoryStorage = () => Module("MemoryStorage")({
41
+ provides: [memoryStorageProvider()],
42
+ exports: [StorageBackend]
43
+ });
44
+ //#endregion
45
+ //#region src/instrument.ts
46
+ /**
47
+ * One operation, wrapped: a span around it, a count of how it came out, and a
48
+ * log line if it failed.
49
+ *
50
+ * **A missing object is counted `not_found` and logged at `info`, not
51
+ * `error`.** Asking for something that is not there is an ordinary answer, and
52
+ * a dashboard that treats it as a fault teaches its readers to ignore the fault
53
+ * line. `StorageUnavailable` is what pages somebody.
54
+ */
55
+ const observed = (call, { operation, key, logger, tracer, operations }) => {
56
+ const span = tracer.startSpan(`storage.${operation}`);
57
+ span.setAttributes({ "btravstack.storage.key": key });
58
+ return call().tap(() => {
59
+ operations.add(1, {
60
+ operation,
61
+ outcome: "ok"
62
+ });
63
+ span.end();
64
+ }).tapFailure((failure) => {
65
+ const cause = failure.tag === "Err" ? failure.error : failure.cause;
66
+ const message = failure.tag !== "Err" ? void 0 : failure.error._tag === "ObjectNotFound" ? "the object was not there" : failure.error._tag === "PresignNotSupported" ? "this store cannot mint a url" : void 0;
67
+ operations.add(1, {
68
+ operation,
69
+ outcome: message === void 0 ? "error" : "not_found"
70
+ });
71
+ if (message === void 0) {
72
+ logger.error("the store could not answer", {
73
+ operation,
74
+ key
75
+ }, cause);
76
+ span.setStatus({ code: SPAN_STATUS.error });
77
+ } else logger.info(message, {
78
+ operation,
79
+ key
80
+ });
81
+ span.end();
82
+ });
83
+ };
84
+ const instrument = (backend, logger, tracer, meter) => {
85
+ const operations = meter.createCounter("btravstack.storage.operations", { description: "Storage operations, by operation and outcome" });
86
+ const context = (operation, key) => ({
87
+ operation,
88
+ key,
89
+ logger,
90
+ tracer,
91
+ operations
92
+ });
93
+ return {
94
+ put: (key, bytes, options) => observed(() => backend.put(key, bytes, options), context("put", key)),
95
+ get: (key) => observed(() => backend.get(key), context("get", key)),
96
+ delete: (key) => observed(() => backend.delete(key), context("delete", key)),
97
+ presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key))
98
+ };
99
+ };
100
+ //#endregion
101
+ //#region src/module.ts
102
+ /**
103
+ * The storage starter: an adapter, and `Storage` provided from it โ€”
104
+ * instrumented or not, decided here at the composition root.
105
+ *
106
+ * ```ts
107
+ * storage({ adapter: s3Storage() }); // spans, counts, log lines
108
+ * storage({ adapter: s3Storage(), instrumented: false }); // just a store
109
+ * ```
110
+ *
111
+ * **Two ports, because di allows one provider per port per graph**: the port an
112
+ * application depends on must not be the one an adapter provides, which is also
113
+ * why instrumentation is a flag on the composition rather than a wrapper.
114
+ */
115
+ const storage = ({ adapter, instrumented }) => instrumented !== false ? Module("InstrumentedStorage")({
116
+ needs: [
117
+ Logger,
118
+ Meter,
119
+ Tracer
120
+ ],
121
+ imports: [adapter],
122
+ provides: [Provider(Storage)({
123
+ backend: StorageBackend,
124
+ logger: Logger,
125
+ tracer: Tracer,
126
+ meter: Meter
127
+ }, { sync: ({ backend, logger, tracer, meter }) => instrument(backend, logger, tracer, meter) })],
128
+ exports: [Storage]
129
+ }) : Module("Storage")({
130
+ imports: [adapter],
131
+ provides: [Provider(Storage)({ backend: StorageBackend }, { sync: ({ backend }) => backend })],
132
+ exports: [Storage]
133
+ });
134
+ //#endregion
135
+ export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, StorageUnavailable, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
package/dist/s3.cjs ADDED
@@ -0,0 +1,103 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_storage = require("./storage-QVPXOcO6.cjs");
3
+ let _btravstack_di = require("@btravstack/di");
4
+ let unthrown = require("unthrown");
5
+ let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
6
+ let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
7
+ let _btravstack_config = require("@btravstack/config");
8
+ //#region src/s3.ts
9
+ /** What the graph bound from the environment for the S3 adapter. */
10
+ var StorageConfig = class extends (0, _btravstack_di.Port)("StorageConfig") {};
11
+ /**
12
+ * The five variables an S3-compatible store needs, and no sixth.
13
+ *
14
+ * `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
15
+ * this port exists for has one, and a default pointing at Amazon would be a
16
+ * surprising bill. **Path-style addressing is not configurable**, it is simply
17
+ * on โ€” every self-hosted store requires it and AWS accepts it, so it is a value
18
+ * that never changes.
19
+ */
20
+ const s3Schema = _btravstack_config.Config.object({
21
+ endpoint: _btravstack_config.Config.string("STORAGE_S3_ENDPOINT"),
22
+ region: _btravstack_config.Config.string("STORAGE_S3_REGION", { default: "us-east-1" }),
23
+ bucket: _btravstack_config.Config.string("STORAGE_S3_BUCKET"),
24
+ accessKeyId: _btravstack_config.Config.string("STORAGE_S3_ACCESS_KEY_ID"),
25
+ secretAccessKey: _btravstack_config.Config.string("STORAGE_S3_SECRET_ACCESS_KEY")
26
+ });
27
+ /**
28
+ * The client, as a port of its own: a resourceful provider is handed back the
29
+ * service it acquired, and a store's four methods are not something you can
30
+ * close โ€” so the client rides the graph and the scope closing destroys it.
31
+ */
32
+ var S3Connection = class extends (0, _btravstack_di.Port)("S3Connection") {};
33
+ const unavailable = (operation, key, cause) => new require_storage.StorageUnavailable({
34
+ operation,
35
+ key,
36
+ reason: cause instanceof Error ? cause.message : String(cause)
37
+ });
38
+ /** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
39
+ const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
40
+ /**
41
+ * The adapter's service over a client and a bucket. A delete of a key nobody
42
+ * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
43
+ * fiction layered over it.
44
+ */
45
+ const s3StorageBackend = (client, bucket) => ({
46
+ put: (key, bytes, options) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.PutObjectCommand({
47
+ Bucket: bucket,
48
+ Key: key,
49
+ Body: bytes,
50
+ ContentType: options.contentType
51
+ })), (cause) => unavailable("put", key, cause)).map(() => void 0),
52
+ get: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.GetObjectCommand({
53
+ Bucket: bucket,
54
+ Key: key
55
+ })).then(async (response) => ({
56
+ bytes: await response.Body.transformToByteArray(),
57
+ contentType: response.ContentType ?? "application/octet-stream"
58
+ })), (cause) => isMissing(cause) ? new require_storage.ObjectNotFound({ key }) : unavailable("get", key, cause)),
59
+ delete: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
60
+ Bucket: bucket,
61
+ Key: key
62
+ })), (cause) => unavailable("delete", key, cause)).map(() => void 0),
63
+ presignedUrl: (key, options) => (0, unthrown.fromPromise)((0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
64
+ Bucket: bucket,
65
+ Key: key
66
+ }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause))
67
+ });
68
+ /**
69
+ * The S3 adapter: one client, built with the scope and destroyed with it, and
70
+ * `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
71
+ * only through this subpath, so an application composing the memory adapter
72
+ * installs neither.
73
+ */
74
+ const s3Storage = () => (0, _btravstack_di.Module)("S3Storage")({
75
+ needs: [_btravstack_config.Env],
76
+ provides: [
77
+ _btravstack_config.Config.provider(StorageConfig)(s3Schema),
78
+ (0, _btravstack_di.Provider)(S3Connection)({ config: StorageConfig }, {
79
+ acquire: ({ config }) => (0, unthrown.fromSafePromise)(Promise.resolve({
80
+ client: new _aws_sdk_client_s3.S3Client({
81
+ endpoint: config.endpoint,
82
+ region: config.region,
83
+ forcePathStyle: true,
84
+ credentials: {
85
+ accessKeyId: config.accessKeyId,
86
+ secretAccessKey: config.secretAccessKey
87
+ }
88
+ }),
89
+ bucket: config.bucket
90
+ })),
91
+ release: ({ client }) => {
92
+ client.destroy();
93
+ }
94
+ }),
95
+ (0, _btravstack_di.Provider)(require_storage.StorageBackend)({ connection: S3Connection }, { sync: ({ connection }) => s3StorageBackend(connection.client, connection.bucket) })
96
+ ],
97
+ exports: [require_storage.StorageBackend]
98
+ });
99
+ //#endregion
100
+ exports.StorageConfig = StorageConfig;
101
+ exports.s3Schema = s3Schema;
102
+ exports.s3Storage = s3Storage;
103
+ exports.s3StorageBackend = s3StorageBackend;
package/dist/s3.d.cts ADDED
@@ -0,0 +1,45 @@
1
+ import { a as StorageService, i as StorageBackend } from "./storage-C9j3zj7r.cjs";
2
+ import { Module, Scope } from "@btravstack/di";
3
+ import { S3Client } from "@aws-sdk/client-s3";
4
+ import { ConfigInvalid, Env } from "@btravstack/config";
5
+ //#region src/s3.d.ts
6
+ declare const StorageConfig_base: import("@btravstack/di").PortClass<"StorageConfig">;
7
+ /** What the graph bound from the environment for the S3 adapter. */
8
+ declare class StorageConfig extends StorageConfig_base<{
9
+ readonly endpoint: string;
10
+ readonly region: string;
11
+ readonly bucket: string;
12
+ readonly accessKeyId: string;
13
+ readonly secretAccessKey: string;
14
+ }> {}
15
+ /**
16
+ * The five variables an S3-compatible store needs, and no sixth.
17
+ *
18
+ * `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
19
+ * this port exists for has one, and a default pointing at Amazon would be a
20
+ * surprising bill. **Path-style addressing is not configurable**, it is simply
21
+ * on โ€” every self-hosted store requires it and AWS accepts it, so it is a value
22
+ * that never changes.
23
+ */
24
+ declare const s3Schema: import("@btravstack/config").ConfigSchema<Readonly<Record<string, string | undefined>>, {
25
+ readonly endpoint: string;
26
+ readonly region: string;
27
+ readonly bucket: string;
28
+ readonly accessKeyId: string;
29
+ readonly secretAccessKey: string;
30
+ }>;
31
+ /**
32
+ * The adapter's service over a client and a bucket. A delete of a key nobody
33
+ * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
34
+ * fiction layered over it.
35
+ */
36
+ declare const s3StorageBackend: (client: S3Client, bucket: string) => StorageService;
37
+ /**
38
+ * The S3 adapter: one client, built with the scope and destroyed with it, and
39
+ * `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
40
+ * only through this subpath, so an application composing the memory adapter
41
+ * installs neither.
42
+ */
43
+ declare const s3Storage: () => Module<StorageBackend, ConfigInvalid, Env | Scope>;
44
+ //#endregion
45
+ export { StorageConfig, s3Schema, s3Storage, s3StorageBackend };
package/dist/s3.d.mts ADDED
@@ -0,0 +1,45 @@
1
+ import { a as StorageService, i as StorageBackend } from "./storage-C9j3zj7r.mjs";
2
+ import { Module, Scope } from "@btravstack/di";
3
+ import { S3Client } from "@aws-sdk/client-s3";
4
+ import { ConfigInvalid, Env } from "@btravstack/config";
5
+ //#region src/s3.d.ts
6
+ declare const StorageConfig_base: import("@btravstack/di").PortClass<"StorageConfig">;
7
+ /** What the graph bound from the environment for the S3 adapter. */
8
+ declare class StorageConfig extends StorageConfig_base<{
9
+ readonly endpoint: string;
10
+ readonly region: string;
11
+ readonly bucket: string;
12
+ readonly accessKeyId: string;
13
+ readonly secretAccessKey: string;
14
+ }> {}
15
+ /**
16
+ * The five variables an S3-compatible store needs, and no sixth.
17
+ *
18
+ * `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
19
+ * this port exists for has one, and a default pointing at Amazon would be a
20
+ * surprising bill. **Path-style addressing is not configurable**, it is simply
21
+ * on โ€” every self-hosted store requires it and AWS accepts it, so it is a value
22
+ * that never changes.
23
+ */
24
+ declare const s3Schema: import("@btravstack/config").ConfigSchema<Readonly<Record<string, string | undefined>>, {
25
+ readonly endpoint: string;
26
+ readonly region: string;
27
+ readonly bucket: string;
28
+ readonly accessKeyId: string;
29
+ readonly secretAccessKey: string;
30
+ }>;
31
+ /**
32
+ * The adapter's service over a client and a bucket. A delete of a key nobody
33
+ * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
34
+ * fiction layered over it.
35
+ */
36
+ declare const s3StorageBackend: (client: S3Client, bucket: string) => StorageService;
37
+ /**
38
+ * The S3 adapter: one client, built with the scope and destroyed with it, and
39
+ * `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
40
+ * only through this subpath, so an application composing the memory adapter
41
+ * installs neither.
42
+ */
43
+ declare const s3Storage: () => Module<StorageBackend, ConfigInvalid, Env | Scope>;
44
+ //#endregion
45
+ export { StorageConfig, s3Schema, s3Storage, s3StorageBackend };
package/dist/s3.mjs ADDED
@@ -0,0 +1,99 @@
1
+ import { a as StorageUnavailable, i as StorageBackend, t as ObjectNotFound } from "./storage-CRkwIhac.mjs";
2
+ import { Module, Port, Provider } from "@btravstack/di";
3
+ import { fromPromise, fromSafePromise } from "unthrown";
4
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
5
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
6
+ import { Config, Env } from "@btravstack/config";
7
+ //#region src/s3.ts
8
+ /** What the graph bound from the environment for the S3 adapter. */
9
+ var StorageConfig = class extends Port("StorageConfig") {};
10
+ /**
11
+ * The five variables an S3-compatible store needs, and no sixth.
12
+ *
13
+ * `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
14
+ * this port exists for has one, and a default pointing at Amazon would be a
15
+ * surprising bill. **Path-style addressing is not configurable**, it is simply
16
+ * on โ€” every self-hosted store requires it and AWS accepts it, so it is a value
17
+ * that never changes.
18
+ */
19
+ const s3Schema = Config.object({
20
+ endpoint: Config.string("STORAGE_S3_ENDPOINT"),
21
+ region: Config.string("STORAGE_S3_REGION", { default: "us-east-1" }),
22
+ bucket: Config.string("STORAGE_S3_BUCKET"),
23
+ accessKeyId: Config.string("STORAGE_S3_ACCESS_KEY_ID"),
24
+ secretAccessKey: Config.string("STORAGE_S3_SECRET_ACCESS_KEY")
25
+ });
26
+ /**
27
+ * The client, as a port of its own: a resourceful provider is handed back the
28
+ * service it acquired, and a store's four methods are not something you can
29
+ * close โ€” so the client rides the graph and the scope closing destroys it.
30
+ */
31
+ var S3Connection = class extends Port("S3Connection") {};
32
+ const unavailable = (operation, key, cause) => new StorageUnavailable({
33
+ operation,
34
+ key,
35
+ reason: cause instanceof Error ? cause.message : String(cause)
36
+ });
37
+ /** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
38
+ const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
39
+ /**
40
+ * The adapter's service over a client and a bucket. A delete of a key nobody
41
+ * stored answers `Ok`, which is S3's own idempotent behaviour rather than a
42
+ * fiction layered over it.
43
+ */
44
+ const s3StorageBackend = (client, bucket) => ({
45
+ put: (key, bytes, options) => fromPromise(client.send(new PutObjectCommand({
46
+ Bucket: bucket,
47
+ Key: key,
48
+ Body: bytes,
49
+ ContentType: options.contentType
50
+ })), (cause) => unavailable("put", key, cause)).map(() => void 0),
51
+ get: (key) => fromPromise(client.send(new GetObjectCommand({
52
+ Bucket: bucket,
53
+ Key: key
54
+ })).then(async (response) => ({
55
+ bytes: await response.Body.transformToByteArray(),
56
+ contentType: response.ContentType ?? "application/octet-stream"
57
+ })), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("get", key, cause)),
58
+ delete: (key) => fromPromise(client.send(new DeleteObjectCommand({
59
+ Bucket: bucket,
60
+ Key: key
61
+ })), (cause) => unavailable("delete", key, cause)).map(() => void 0),
62
+ presignedUrl: (key, options) => fromPromise(getSignedUrl(client, new GetObjectCommand({
63
+ Bucket: bucket,
64
+ Key: key
65
+ }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause))
66
+ });
67
+ /**
68
+ * The S3 adapter: one client, built with the scope and destroyed with it, and
69
+ * `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
70
+ * only through this subpath, so an application composing the memory adapter
71
+ * installs neither.
72
+ */
73
+ const s3Storage = () => Module("S3Storage")({
74
+ needs: [Env],
75
+ provides: [
76
+ Config.provider(StorageConfig)(s3Schema),
77
+ Provider(S3Connection)({ config: StorageConfig }, {
78
+ acquire: ({ config }) => fromSafePromise(Promise.resolve({
79
+ client: new S3Client({
80
+ endpoint: config.endpoint,
81
+ region: config.region,
82
+ forcePathStyle: true,
83
+ credentials: {
84
+ accessKeyId: config.accessKeyId,
85
+ secretAccessKey: config.secretAccessKey
86
+ }
87
+ }),
88
+ bucket: config.bucket
89
+ })),
90
+ release: ({ client }) => {
91
+ client.destroy();
92
+ }
93
+ }),
94
+ Provider(StorageBackend)({ connection: S3Connection }, { sync: ({ connection }) => s3StorageBackend(connection.client, connection.bucket) })
95
+ ],
96
+ exports: [StorageBackend]
97
+ });
98
+ //#endregion
99
+ export { StorageConfig, s3Schema, s3Storage, s3StorageBackend };
@@ -0,0 +1,61 @@
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` answers it โ€” see `delete` and `presignedUrl` below. */
10
+ declare class ObjectNotFound extends ObjectNotFound_base<{
11
+ readonly key: string;
12
+ }> {}
13
+ declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
14
+ /** The store could not answer โ€” the endpoint is down, the credentials were refused, the bucket is gone. */
15
+ declare class StorageUnavailable extends StorageUnavailable_base<{
16
+ readonly operation: "put" | "get" | "delete" | "presignedUrl";
17
+ readonly key: string;
18
+ readonly reason: string;
19
+ }> {}
20
+ declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructor<"PresignNotSupported">;
21
+ /** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
22
+ declare class PresignNotSupported extends PresignNotSupported_base<{
23
+ readonly key: string;
24
+ }> {}
25
+ type StorageService = {
26
+ readonly put: (key: string, bytes: Uint8Array, options: {
27
+ readonly contentType: string;
28
+ }) => AsyncResult<void, StorageUnavailable>;
29
+ readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
30
+ /** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
31
+ readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
32
+ /**
33
+ * A URL that reads the object for `ttlMs`, without the caller holding
34
+ * credentials.
35
+ *
36
+ * There is **no `ObjectNotFound` arm**: presigning is a signature computation
37
+ * that asks the store nothing, so a URL for an absent key is minted happily
38
+ * and 404s when followed. Checking would cost a HEAD per call.
39
+ */
40
+ readonly presignedUrl: (key: string, options: {
41
+ readonly ttlMs: number;
42
+ }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
43
+ };
44
+ declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
45
+ /**
46
+ * The port an application depends on.
47
+ *
48
+ * Keys are plain strings the caller composes, tenant included: a store is an
49
+ * application service, and the framework has no concept of a tenant to put in a
50
+ * slot.
51
+ *
52
+ * **Bytes, not streams.** An object here is a document. Streaming would change
53
+ * every signature, adapter and test to serve a case that wants a different
54
+ * design anyway โ€” a stated non-goal, not an oversight.
55
+ */
56
+ declare class Storage extends Storage_base<StorageService> {}
57
+ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBackend">;
58
+ /** The port every adapter provides, and the one an application never depends on. */
59
+ declare class StorageBackend extends StorageBackend_base<StorageService> {}
60
+ //#endregion
61
+ export { StorageService as a, StorageBackend as i, PresignNotSupported as n, StorageUnavailable as o, Storage as r, StoredObject as s, ObjectNotFound as t };
@@ -0,0 +1,61 @@
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` answers it โ€” see `delete` and `presignedUrl` below. */
10
+ declare class ObjectNotFound extends ObjectNotFound_base<{
11
+ readonly key: string;
12
+ }> {}
13
+ declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
14
+ /** The store could not answer โ€” the endpoint is down, the credentials were refused, the bucket is gone. */
15
+ declare class StorageUnavailable extends StorageUnavailable_base<{
16
+ readonly operation: "put" | "get" | "delete" | "presignedUrl";
17
+ readonly key: string;
18
+ readonly reason: string;
19
+ }> {}
20
+ declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructor<"PresignNotSupported">;
21
+ /** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
22
+ declare class PresignNotSupported extends PresignNotSupported_base<{
23
+ readonly key: string;
24
+ }> {}
25
+ type StorageService = {
26
+ readonly put: (key: string, bytes: Uint8Array, options: {
27
+ readonly contentType: string;
28
+ }) => AsyncResult<void, StorageUnavailable>;
29
+ readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
30
+ /** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
31
+ readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
32
+ /**
33
+ * A URL that reads the object for `ttlMs`, without the caller holding
34
+ * credentials.
35
+ *
36
+ * There is **no `ObjectNotFound` arm**: presigning is a signature computation
37
+ * that asks the store nothing, so a URL for an absent key is minted happily
38
+ * and 404s when followed. Checking would cost a HEAD per call.
39
+ */
40
+ readonly presignedUrl: (key: string, options: {
41
+ readonly ttlMs: number;
42
+ }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
43
+ };
44
+ declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
45
+ /**
46
+ * The port an application depends on.
47
+ *
48
+ * Keys are plain strings the caller composes, tenant included: a store is an
49
+ * application service, and the framework has no concept of a tenant to put in a
50
+ * slot.
51
+ *
52
+ * **Bytes, not streams.** An object here is a document. Streaming would change
53
+ * every signature, adapter and test to serve a case that wants a different
54
+ * design anyway โ€” a stated non-goal, not an oversight.
55
+ */
56
+ declare class Storage extends Storage_base<StorageService> {}
57
+ declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBackend">;
58
+ /** The port every adapter provides, and the one an application never depends on. */
59
+ declare class StorageBackend extends StorageBackend_base<StorageService> {}
60
+ //#endregion
61
+ export { StorageService as a, StorageBackend as i, PresignNotSupported as n, StorageUnavailable as o, Storage as r, StoredObject as s, ObjectNotFound as t };
@@ -0,0 +1,25 @@
1
+ import { Port } from "@btravstack/di";
2
+ import { TaggedError } from "unthrown";
3
+ //#region src/storage.ts
4
+ /** The key is not there. Only `get` answers it โ€” see `delete` and `presignedUrl` below. */
5
+ var ObjectNotFound = class extends TaggedError("ObjectNotFound") {};
6
+ /** The store could not answer โ€” the endpoint is down, the credentials were refused, the bucket is gone. */
7
+ var StorageUnavailable = class extends TaggedError("StorageUnavailable") {};
8
+ /** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
9
+ var PresignNotSupported = class extends TaggedError("PresignNotSupported") {};
10
+ /**
11
+ * The port an application depends on.
12
+ *
13
+ * Keys are plain strings the caller composes, tenant included: a store is an
14
+ * application service, and the framework has no concept of a tenant to put in a
15
+ * slot.
16
+ *
17
+ * **Bytes, not streams.** An object here is a document. Streaming would change
18
+ * every signature, adapter and test to serve a case that wants a different
19
+ * design anyway โ€” a stated non-goal, not an oversight.
20
+ */
21
+ var Storage = class extends Port("Storage") {};
22
+ /** The port every adapter provides, and the one an application never depends on. */
23
+ var StorageBackend = class extends Port("StorageBackend") {};
24
+ //#endregion
25
+ export { StorageUnavailable as a, StorageBackend as i, PresignNotSupported as n, Storage as r, ObjectNotFound as t };
@@ -0,0 +1,54 @@
1
+ let _btravstack_di = require("@btravstack/di");
2
+ let unthrown = require("unthrown");
3
+ //#region src/storage.ts
4
+ /** The key is not there. Only `get` answers it โ€” see `delete` and `presignedUrl` below. */
5
+ var ObjectNotFound = class extends (0, unthrown.TaggedError)("ObjectNotFound") {};
6
+ /** The store could not answer โ€” the endpoint is down, the credentials were refused, the bucket is gone. */
7
+ var StorageUnavailable = class extends (0, unthrown.TaggedError)("StorageUnavailable") {};
8
+ /** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
9
+ var PresignNotSupported = class extends (0, unthrown.TaggedError)("PresignNotSupported") {};
10
+ /**
11
+ * The port an application depends on.
12
+ *
13
+ * Keys are plain strings the caller composes, tenant included: a store is an
14
+ * application service, and the framework has no concept of a tenant to put in a
15
+ * slot.
16
+ *
17
+ * **Bytes, not streams.** An object here is a document. Streaming would change
18
+ * every signature, adapter and test to serve a case that wants a different
19
+ * design anyway โ€” a stated non-goal, not an oversight.
20
+ */
21
+ var Storage = class extends (0, _btravstack_di.Port)("Storage") {};
22
+ /** The port every adapter provides, and the one an application never depends on. */
23
+ var StorageBackend = class extends (0, _btravstack_di.Port)("StorageBackend") {};
24
+ //#endregion
25
+ Object.defineProperty(exports, "ObjectNotFound", {
26
+ enumerable: true,
27
+ get: function() {
28
+ return ObjectNotFound;
29
+ }
30
+ });
31
+ Object.defineProperty(exports, "PresignNotSupported", {
32
+ enumerable: true,
33
+ get: function() {
34
+ return PresignNotSupported;
35
+ }
36
+ });
37
+ Object.defineProperty(exports, "Storage", {
38
+ enumerable: true,
39
+ get: function() {
40
+ return Storage;
41
+ }
42
+ });
43
+ Object.defineProperty(exports, "StorageBackend", {
44
+ enumerable: true,
45
+ get: function() {
46
+ return StorageBackend;
47
+ }
48
+ });
49
+ Object.defineProperty(exports, "StorageUnavailable", {
50
+ enumerable: true,
51
+ get: function() {
52
+ return StorageUnavailable;
53
+ }
54
+ });
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@btravstack/storage",
3
+ "version": "0.3.0",
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
+ "keywords": [
6
+ "dependency-injection",
7
+ "object-storage",
8
+ "s3",
9
+ "storage",
10
+ "typescript",
11
+ "unthrown"
12
+ ],
13
+ "homepage": "https://github.com/btravstack/btravstack#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/btravstack/btravstack/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/btravstack/btravstack.git",
22
+ "directory": "packages/storage"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.mjs",
31
+ "types": "./dist/index.d.cts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.mts",
36
+ "default": "./dist/index.mjs"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "./s3": {
44
+ "import": {
45
+ "types": "./dist/s3.d.mts",
46
+ "default": "./dist/s3.mjs"
47
+ },
48
+ "require": {
49
+ "types": "./dist/s3.d.cts",
50
+ "default": "./dist/s3.cjs"
51
+ }
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
55
+ "devDependencies": {
56
+ "@aws-sdk/client-s3": "3.1117.0",
57
+ "@aws-sdk/s3-request-presigner": "3.1117.0",
58
+ "@btravstack/tsconfig": "0.2.0",
59
+ "@opentelemetry/api": "1.9.1",
60
+ "@opentelemetry/sdk-metrics": "2.10.0",
61
+ "@opentelemetry/sdk-node": "0.221.0",
62
+ "@opentelemetry/sdk-trace": "2.10.0",
63
+ "@types/node": "26.2.0",
64
+ "@unthrown/vitest": "5.7.0",
65
+ "@vitest/coverage-v8": "4.1.11",
66
+ "tsdown": "0.22.14",
67
+ "typescript": "7.0.2",
68
+ "unthrown": "5.7.0",
69
+ "vitest": "4.1.11",
70
+ "@btravstack/config": "0.3.0",
71
+ "@btravstack/internal-test-infra": "0.0.0",
72
+ "@btravstack/core": "0.3.0",
73
+ "@btravstack/observability": "0.3.0",
74
+ "@btravstack/di": "0.3.0"
75
+ },
76
+ "peerDependencies": {
77
+ "@aws-sdk/client-s3": "^3.1000.0",
78
+ "@aws-sdk/s3-request-presigner": "^3.1000.0",
79
+ "unthrown": "^5.0.0",
80
+ "@btravstack/config": "^0.3.0",
81
+ "@btravstack/di": "^0.3.0",
82
+ "@btravstack/core": "^0.3.0"
83
+ },
84
+ "peerDependenciesMeta": {
85
+ "@aws-sdk/client-s3": {
86
+ "optional": true
87
+ },
88
+ "@aws-sdk/s3-request-presigner": {
89
+ "optional": true
90
+ }
91
+ },
92
+ "engines": {
93
+ "node": ">=20"
94
+ },
95
+ "scripts": {
96
+ "build": "tsdown src/index.ts src/s3.ts --format cjs,esm --dts --clean",
97
+ "dev": "tsdown src/index.ts src/s3.ts --format cjs,esm --dts --watch",
98
+ "test": "vitest run --coverage",
99
+ "test:types": "tsc -p tsconfig.test-d.json --noEmit",
100
+ "typecheck": "tsc --noEmit"
101
+ }
102
+ }