@btravstack/storage 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > The object-storage port for [`@btravstack/core`](../core): one `Storage` an
4
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.
5
+ > presigned reads and writes, and a span, a count and a log line per operation.
6
6
 
7
7
  📖 **[Documentation](https://btravstack.github.io/btravstack/reference/storage)** ·
8
8
  [API Reference](https://btravstack.github.io/btravstack/api/storage/)
@@ -129,5 +129,5 @@ says so instead of minting a URL that would fail only in production.
129
129
 
130
130
  **It does not decide** what your keys look like, whether a failed write should
131
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
132
+ no listing and no bucket management; the reasons are in
133
133
  [`CLAUDE.md`](./CLAUDE.md).
package/dist/index.cjs CHANGED
@@ -32,7 +32,8 @@ const memoryStorageBackend = () => {
32
32
  objects.delete(key);
33
33
  return (0, unthrown.OkAsync)();
34
34
  },
35
- presignedUrl: (key) => (0, unthrown.ErrAsync)(new require_storage.PresignNotSupported({ key }))
35
+ presignedUrl: (key) => (0, unthrown.ErrAsync)(new require_storage.PresignNotSupported({ key })),
36
+ presignedUpload: (key) => (0, unthrown.ErrAsync)(new require_storage.PresignNotSupported({ key }))
36
37
  };
37
38
  };
38
39
  /** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
@@ -95,7 +96,8 @@ const instrument = (backend, logger, tracer, meter) => {
95
96
  put: (key, bytes, options) => observed(() => backend.put(key, bytes, options), context("put", key)),
96
97
  get: (key) => observed(() => backend.get(key), context("get", key)),
97
98
  delete: (key) => observed(() => backend.delete(key), context("delete", key)),
98
- presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key))
99
+ presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key)),
100
+ presignedUpload: (key, options) => observed(() => backend.presignedUpload(key, options), context("presigned_upload", key))
99
101
  };
100
102
  };
101
103
  //#endregion
@@ -113,25 +115,31 @@ const instrument = (backend, logger, tracer, meter) => {
113
115
  * application depends on must not be the one an adapter provides, which is also
114
116
  * why instrumentation is a flag on the composition rather than a wrapper.
115
117
  */
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
- });
118
+ const storage = ({ adapter, instrumented }) => {
119
+ const healthCheck = _btravstack_di.Provider.member(_btravstack_core.HealthChecks)({ backend: require_storage.StorageBackend }, { sync: ({ backend }) => ({
120
+ name: "storage",
121
+ check: () => backend.get("btravstack:health").map(() => void 0).flatMapErrCases((matcher) => matcher.with(unthrown.P.tag("ObjectNotFound"), () => (0, unthrown.Ok)(void 0)).with(unthrown.P.tag("StorageUnavailable"), ({ reason }) => (0, unthrown.Err)(new _btravstack_core.HealthCheckFailed({ reason: `storage unavailable (${reason})` }))))
122
+ }) });
123
+ return instrumented !== false ? (0, _btravstack_di.Module)("InstrumentedStorage")({
124
+ needs: [
125
+ _btravstack_core.Logger,
126
+ _btravstack_core.Meter,
127
+ _btravstack_core.Tracer
128
+ ],
129
+ imports: [adapter],
130
+ provides: [(0, _btravstack_di.Provider)(require_storage.Storage)({
131
+ backend: require_storage.StorageBackend,
132
+ logger: _btravstack_core.Logger,
133
+ tracer: _btravstack_core.Tracer,
134
+ meter: _btravstack_core.Meter
135
+ }, { sync: ({ backend, logger, tracer, meter }) => instrument(backend, logger, tracer, meter) }), healthCheck],
136
+ exports: [require_storage.Storage, _btravstack_core.HealthChecks]
137
+ }) : (0, _btravstack_di.Module)("Storage")({
138
+ imports: [adapter],
139
+ provides: [(0, _btravstack_di.Provider)(require_storage.Storage)({ backend: require_storage.StorageBackend }, { sync: ({ backend }) => backend }), healthCheck],
140
+ exports: [require_storage.Storage, _btravstack_core.HealthChecks]
141
+ });
142
+ };
135
143
  //#endregion
136
144
  exports.ObjectNotFound = require_storage.ObjectNotFound;
137
145
  exports.PresignNotSupported = require_storage.PresignNotSupported;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
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";
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-Cu27C7Oq.cjs";
2
2
  import { Module, Provider } from "@btravstack/di";
3
- import { Logger, Meter, Tracer } from "@btravstack/core";
3
+ import { HealthChecks, Logger, Meter, Tracer } from "@btravstack/core";
4
4
  //#region src/memory.d.ts
5
5
  /**
6
6
  * The in-process adapter: a `Map`, and an honest refusal to presign.
@@ -51,6 +51,6 @@ type StorageOptions<E, N, Instrumented extends boolean> = {
51
51
  * application depends on must not be the one an adapter provides, which is also
52
52
  * why instrumentation is a flag on the composition rather than a wrapper.
53
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>;
54
+ declare const storage: <E, N, Instrumented extends boolean = true>({ adapter, instrumented }: StorageOptions<E, N, Instrumented>) => Module<Storage | HealthChecks, E, Instrumented extends true ? N | Logger | Meter | Tracer : N>;
55
55
  //#endregion
56
56
  export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
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";
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-Cu27C7Oq.mjs";
2
2
  import { Module, Provider } from "@btravstack/di";
3
- import { Logger, Meter, Tracer } from "@btravstack/core";
3
+ import { HealthChecks, Logger, Meter, Tracer } from "@btravstack/core";
4
4
  //#region src/memory.d.ts
5
5
  /**
6
6
  * The in-process adapter: a `Map`, and an honest refusal to presign.
@@ -51,6 +51,6 @@ type StorageOptions<E, N, Instrumented extends boolean> = {
51
51
  * application depends on must not be the one an adapter provides, which is also
52
52
  * why instrumentation is a flag on the composition rather than a wrapper.
53
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>;
54
+ declare const storage: <E, N, Instrumented extends boolean = true>({ adapter, instrumented }: StorageOptions<E, N, Instrumented>) => Module<Storage | HealthChecks, E, Instrumented extends true ? N | Logger | Meter | Tracer : N>;
55
55
  //#endregion
56
56
  export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as StorageUnavailable, i as StorageBackend, n as PresignNotSupported, r as Storage, t as ObjectNotFound } from "./storage-CRkwIhac.mjs";
2
2
  import { Module, Provider } from "@btravstack/di";
3
- import { ErrAsync, OkAsync } from "unthrown";
4
- import { Logger, Meter, SPAN_STATUS, Tracer } from "@btravstack/core";
3
+ import { Err, ErrAsync, Ok, OkAsync, P } from "unthrown";
4
+ import { HealthCheckFailed, HealthChecks, Logger, Meter, SPAN_STATUS, Tracer } from "@btravstack/core";
5
5
  //#region src/memory.ts
6
6
  /**
7
7
  * The in-process adapter: a `Map`, and an honest refusal to presign.
@@ -31,7 +31,8 @@ const memoryStorageBackend = () => {
31
31
  objects.delete(key);
32
32
  return OkAsync();
33
33
  },
34
- presignedUrl: (key) => ErrAsync(new PresignNotSupported({ key }))
34
+ presignedUrl: (key) => ErrAsync(new PresignNotSupported({ key })),
35
+ presignedUpload: (key) => ErrAsync(new PresignNotSupported({ key }))
35
36
  };
36
37
  };
37
38
  /** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
@@ -94,7 +95,8 @@ const instrument = (backend, logger, tracer, meter) => {
94
95
  put: (key, bytes, options) => observed(() => backend.put(key, bytes, options), context("put", key)),
95
96
  get: (key) => observed(() => backend.get(key), context("get", key)),
96
97
  delete: (key) => observed(() => backend.delete(key), context("delete", key)),
97
- presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key))
98
+ presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key)),
99
+ presignedUpload: (key, options) => observed(() => backend.presignedUpload(key, options), context("presigned_upload", key))
98
100
  };
99
101
  };
100
102
  //#endregion
@@ -112,24 +114,30 @@ const instrument = (backend, logger, tracer, meter) => {
112
114
  * application depends on must not be the one an adapter provides, which is also
113
115
  * why instrumentation is a flag on the composition rather than a wrapper.
114
116
  */
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
- });
117
+ const storage = ({ adapter, instrumented }) => {
118
+ const healthCheck = Provider.member(HealthChecks)({ backend: StorageBackend }, { sync: ({ backend }) => ({
119
+ name: "storage",
120
+ check: () => backend.get("btravstack:health").map(() => void 0).flatMapErrCases((matcher) => matcher.with(P.tag("ObjectNotFound"), () => Ok(void 0)).with(P.tag("StorageUnavailable"), ({ reason }) => Err(new HealthCheckFailed({ reason: `storage unavailable (${reason})` }))))
121
+ }) });
122
+ return instrumented !== false ? Module("InstrumentedStorage")({
123
+ needs: [
124
+ Logger,
125
+ Meter,
126
+ Tracer
127
+ ],
128
+ imports: [adapter],
129
+ provides: [Provider(Storage)({
130
+ backend: StorageBackend,
131
+ logger: Logger,
132
+ tracer: Tracer,
133
+ meter: Meter
134
+ }, { sync: ({ backend, logger, tracer, meter }) => instrument(backend, logger, tracer, meter) }), healthCheck],
135
+ exports: [Storage, HealthChecks]
136
+ }) : Module("Storage")({
137
+ imports: [adapter],
138
+ provides: [Provider(Storage)({ backend: StorageBackend }, { sync: ({ backend }) => backend }), healthCheck],
139
+ exports: [Storage, HealthChecks]
140
+ });
141
+ };
134
142
  //#endregion
135
143
  export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, StorageUnavailable, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
package/dist/s3.cjs CHANGED
@@ -63,7 +63,13 @@ const s3StorageBackend = (client, bucket) => ({
63
63
  presignedUrl: (key, options) => (0, unthrown.fromPromise)((0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
64
64
  Bucket: bucket,
65
65
  Key: key
66
- }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause))
66
+ }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause)),
67
+ presignedUpload: (key, options) => (0, unthrown.fromPromise)((0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.PutObjectCommand({
68
+ Bucket: bucket,
69
+ Key: key,
70
+ ContentType: options.contentType,
71
+ ContentLength: options.contentLength
72
+ }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUpload", key, cause))
67
73
  });
68
74
  /**
69
75
  * The S3 adapter: one client, built with the scope and destroyed with it, and
package/dist/s3.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as StorageService, i as StorageBackend } from "./storage-C9j3zj7r.cjs";
1
+ import { a as StorageService, i as StorageBackend } from "./storage-Cu27C7Oq.cjs";
2
2
  import { Module, Scope } from "@btravstack/di";
3
3
  import { S3Client } from "@aws-sdk/client-s3";
4
4
  import { ConfigInvalid, Env } from "@btravstack/config";
package/dist/s3.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as StorageService, i as StorageBackend } from "./storage-C9j3zj7r.mjs";
1
+ import { a as StorageService, i as StorageBackend } from "./storage-Cu27C7Oq.mjs";
2
2
  import { Module, Scope } from "@btravstack/di";
3
3
  import { S3Client } from "@aws-sdk/client-s3";
4
4
  import { ConfigInvalid, Env } from "@btravstack/config";
package/dist/s3.mjs CHANGED
@@ -62,7 +62,13 @@ const s3StorageBackend = (client, bucket) => ({
62
62
  presignedUrl: (key, options) => fromPromise(getSignedUrl(client, new GetObjectCommand({
63
63
  Bucket: bucket,
64
64
  Key: key
65
- }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause))
65
+ }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause)),
66
+ presignedUpload: (key, options) => fromPromise(getSignedUrl(client, new PutObjectCommand({
67
+ Bucket: bucket,
68
+ Key: key,
69
+ ContentType: options.contentType,
70
+ ContentLength: options.contentLength
71
+ }), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUpload", key, cause))
66
72
  });
67
73
  /**
68
74
  * The S3 adapter: one client, built with the scope and destroyed with it, and
@@ -13,7 +13,7 @@ declare class ObjectNotFound extends ObjectNotFound_base<{
13
13
  declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
14
14
  /** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
15
15
  declare class StorageUnavailable extends StorageUnavailable_base<{
16
- readonly operation: "put" | "get" | "delete" | "presignedUrl";
16
+ readonly operation: "put" | "get" | "delete" | "presignedUrl" | "presignedUpload";
17
17
  readonly key: string;
18
18
  readonly reason: string;
19
19
  }> {}
@@ -40,6 +40,27 @@ type StorageService = {
40
40
  readonly presignedUrl: (key: string, options: {
41
41
  readonly ttlMs: number;
42
42
  }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
43
+ /**
44
+ * A URL that writes the object for `ttlMs`, without the caller holding
45
+ * credentials — so the bytes go from the client to the store and never
46
+ * through this process.
47
+ *
48
+ * **`contentType` and `contentLength` are signed, not advisory.** They are
49
+ * part of the signature, so a client sending different ones is refused by the
50
+ * store: the URL grants exactly one write, of exactly that many bytes, of
51
+ * exactly that type. `contentLength` is therefore required — an unsigned
52
+ * length would hand out an unbounded write, and there is no ceiling a
53
+ * presigned PUT can express other than the exact number.
54
+ *
55
+ * The application still decides every input: whether this caller may write
56
+ * this key, how long the URL lives, and what size it is willing to accept.
57
+ * The adapter computes a signature over that decision and nothing more.
58
+ */
59
+ readonly presignedUpload: (key: string, options: {
60
+ readonly ttlMs: number;
61
+ readonly contentType: string;
62
+ readonly contentLength: number;
63
+ }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
43
64
  };
44
65
  declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
45
66
  /**
@@ -13,7 +13,7 @@ declare class ObjectNotFound extends ObjectNotFound_base<{
13
13
  declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
14
14
  /** The store could not answer — the endpoint is down, the credentials were refused, the bucket is gone. */
15
15
  declare class StorageUnavailable extends StorageUnavailable_base<{
16
- readonly operation: "put" | "get" | "delete" | "presignedUrl";
16
+ readonly operation: "put" | "get" | "delete" | "presignedUrl" | "presignedUpload";
17
17
  readonly key: string;
18
18
  readonly reason: string;
19
19
  }> {}
@@ -40,6 +40,27 @@ type StorageService = {
40
40
  readonly presignedUrl: (key: string, options: {
41
41
  readonly ttlMs: number;
42
42
  }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
43
+ /**
44
+ * A URL that writes the object for `ttlMs`, without the caller holding
45
+ * credentials — so the bytes go from the client to the store and never
46
+ * through this process.
47
+ *
48
+ * **`contentType` and `contentLength` are signed, not advisory.** They are
49
+ * part of the signature, so a client sending different ones is refused by the
50
+ * store: the URL grants exactly one write, of exactly that many bytes, of
51
+ * exactly that type. `contentLength` is therefore required — an unsigned
52
+ * length would hand out an unbounded write, and there is no ceiling a
53
+ * presigned PUT can express other than the exact number.
54
+ *
55
+ * The application still decides every input: whether this caller may write
56
+ * this key, how long the URL lives, and what size it is willing to accept.
57
+ * The adapter computes a signature over that decision and nothing more.
58
+ */
59
+ readonly presignedUpload: (key: string, options: {
60
+ readonly ttlMs: number;
61
+ readonly contentType: string;
62
+ readonly contentLength: number;
63
+ }) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
43
64
  };
44
65
  declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
45
66
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/storage",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "A Storage port for @btravstack/core: an in-memory adapter, an S3-compatible adapter with presigned reads, and a composition that spans, counts and logs every operation",
5
5
  "keywords": [
6
6
  "dependency-injection",
@@ -67,19 +67,19 @@
67
67
  "typescript": "7.0.2",
68
68
  "unthrown": "5.7.0",
69
69
  "vitest": "4.1.11",
70
- "@btravstack/config": "0.5.0",
71
- "@btravstack/core": "0.5.0",
72
- "@btravstack/di": "0.5.0",
73
- "@btravstack/internal-test-infra": "0.0.0",
74
- "@btravstack/observability": "0.5.0"
70
+ "@btravstack/config": "0.7.0",
71
+ "@btravstack/core": "0.7.0",
72
+ "@btravstack/observability": "0.7.0",
73
+ "@btravstack/di": "0.7.0",
74
+ "@btravstack/internal-test-infra": "0.0.0"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@aws-sdk/client-s3": "^3.1000.0",
78
78
  "@aws-sdk/s3-request-presigner": "^3.1000.0",
79
79
  "unthrown": "^5.0.0",
80
- "@btravstack/config": "^0.5.0",
81
- "@btravstack/core": "^0.5.0",
82
- "@btravstack/di": "^0.5.0"
80
+ "@btravstack/config": "^0.7.0",
81
+ "@btravstack/di": "^0.7.0",
82
+ "@btravstack/core": "^0.7.0"
83
83
  },
84
84
  "peerDependenciesMeta": {
85
85
  "@aws-sdk/client-s3": {