@orthacms/media-provider-s3 0.3.0 → 0.4.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.
@@ -0,0 +1,50 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ /** One object, as the fake holds it. */
3
+ interface FakeObject {
4
+ body: Buffer;
5
+ contentType?: string;
6
+ }
7
+ /**
8
+ * A stand-in `S3Client` that answers the four commands this adapter sends.
9
+ *
10
+ * **What it is for and what it is not.** It exercises *our* code — key shapes,
11
+ * the metering that produces size and checksum, the error mapping, the abort
12
+ * path — offline and in milliseconds. It is not a claim that the adapter works
13
+ * against S3: a fake agrees with whatever the code does, which is exactly the
14
+ * failure mode MinIO and a real bucket exist to catch. Both are named in
15
+ * `AGENTS.md` as required before this provider is trusted with a deployment.
16
+ *
17
+ * Exported from `src/` rather than hidden in a spec so the eventual MinIO suite
18
+ * can be written as the same cases against a different client.
19
+ */
20
+ export declare class FakeS3Client {
21
+ private readonly buckets;
22
+ readonly objects: Map<string, FakeObject>;
23
+ /** Every command class name this client was asked to send, in order. */
24
+ readonly sent: string[];
25
+ /** Buckets that exist. `HeadBucket` fails for anything else. */
26
+ constructor(buckets?: Set<string>);
27
+ /** The provider only ever calls `send`, so that is all this implements. */
28
+ send(command: {
29
+ constructor: {
30
+ name: string;
31
+ };
32
+ input: Record<string, unknown>;
33
+ }): Promise<unknown>;
34
+ /** Keys currently held, sorted — the harness for the contract suite. */
35
+ keys(): string[];
36
+ /**
37
+ * A **real** `S3Client` with only its `send` replaced.
38
+ *
39
+ * Hand-rolling the whole client does not work and should not be attempted:
40
+ * `lib-storage` reads the client's own `config` — `requestHandler` for
41
+ * progress events, `endpointProvider` to build the uploaded object's
42
+ * `Location` — so a plain object fails inside the SDK rather than in our
43
+ * code. Keeping the real client means the command construction, the
44
+ * endpoint resolution and the middleware stack are all genuine, and only
45
+ * the network hop is stubbed.
46
+ */
47
+ asClient(): S3Client;
48
+ }
49
+ export {};
50
+ //# sourceMappingURL=fake-s3-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-s3-client.d.ts","sourceRoot":"","sources":["../../src/lib/fake-s3-client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,wCAAwC;AACxC,UAAU,UAAU;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAqBD;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;IAKT,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,QAAQ,CAAC,OAAO,0BAAiC;IACjD,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAM;IAC7B,gEAAgE;gBACnC,OAAO,GAAE,GAAG,CAAC,MAAM,CAAuB;IAEvE,2EAA2E;IACrE,IAAI,CAAC,OAAO,EAAE;QAChB,WAAW,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,GAAG,OAAO,CAAC,OAAO,CAAC;IAuCpB,wEAAwE;IACxE,IAAI,IAAI,MAAM,EAAE;IAIhB;;;;;;;;;;OAUG;IACH,QAAQ,IAAI,QAAQ;CAcvB"}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FakeS3Client = void 0;
4
+ const node_stream_1 = require("node:stream");
5
+ const client_s3_1 = require("@aws-sdk/client-s3");
6
+ /** The error shape this family of services uses for a missing key. */
7
+ function notFound(name) {
8
+ return Object.assign(new Error(name), {
9
+ name,
10
+ $metadata: { httpStatusCode: 404 }
11
+ });
12
+ }
13
+ /** Drains whatever `Body` the SDK was handed into one buffer. */
14
+ async function collect(body) {
15
+ if (Buffer.isBuffer(body))
16
+ return body;
17
+ if (typeof body === 'string')
18
+ return Buffer.from(body);
19
+ const chunks = [];
20
+ for await (const chunk of body) {
21
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
22
+ }
23
+ return Buffer.concat(chunks);
24
+ }
25
+ /**
26
+ * A stand-in `S3Client` that answers the four commands this adapter sends.
27
+ *
28
+ * **What it is for and what it is not.** It exercises *our* code — key shapes,
29
+ * the metering that produces size and checksum, the error mapping, the abort
30
+ * path — offline and in milliseconds. It is not a claim that the adapter works
31
+ * against S3: a fake agrees with whatever the code does, which is exactly the
32
+ * failure mode MinIO and a real bucket exist to catch. Both are named in
33
+ * `AGENTS.md` as required before this provider is trusted with a deployment.
34
+ *
35
+ * Exported from `src/` rather than hidden in a spec so the eventual MinIO suite
36
+ * can be written as the same cases against a different client.
37
+ */
38
+ class FakeS3Client {
39
+ buckets;
40
+ objects = new Map();
41
+ /** Every command class name this client was asked to send, in order. */
42
+ sent = [];
43
+ /** Buckets that exist. `HeadBucket` fails for anything else. */
44
+ constructor(buckets = new Set(['bucket'])) {
45
+ this.buckets = buckets;
46
+ }
47
+ /** The provider only ever calls `send`, so that is all this implements. */
48
+ async send(command) {
49
+ const kind = command.constructor.name;
50
+ this.sent.push(kind);
51
+ const key = command.input['Key'];
52
+ switch (kind) {
53
+ case 'PutObjectCommand': {
54
+ this.objects.set(key, {
55
+ body: await collect(command.input['Body']),
56
+ contentType: command.input['ContentType']
57
+ });
58
+ return { ETag: '"fake"' };
59
+ }
60
+ case 'GetObjectCommand': {
61
+ const object = this.objects.get(key);
62
+ if (!object)
63
+ throw notFound('NoSuchKey');
64
+ return {
65
+ Body: node_stream_1.Readable.from(object.body),
66
+ ContentType: object.contentType
67
+ };
68
+ }
69
+ case 'DeleteObjectCommand': {
70
+ // S3 answers 204 whether or not the key was there.
71
+ this.objects.delete(key);
72
+ return {};
73
+ }
74
+ case 'HeadBucketCommand': {
75
+ if (!this.buckets.has(command.input['Bucket'])) {
76
+ throw notFound('NotFound');
77
+ }
78
+ return {};
79
+ }
80
+ default:
81
+ throw new Error(`FakeS3Client cannot answer ${kind}`);
82
+ }
83
+ }
84
+ /** Keys currently held, sorted — the harness for the contract suite. */
85
+ keys() {
86
+ return [...this.objects.keys()].sort();
87
+ }
88
+ /**
89
+ * A **real** `S3Client` with only its `send` replaced.
90
+ *
91
+ * Hand-rolling the whole client does not work and should not be attempted:
92
+ * `lib-storage` reads the client's own `config` — `requestHandler` for
93
+ * progress events, `endpointProvider` to build the uploaded object's
94
+ * `Location` — so a plain object fails inside the SDK rather than in our
95
+ * code. Keeping the real client means the command construction, the
96
+ * endpoint resolution and the middleware stack are all genuine, and only
97
+ * the network hop is stubbed.
98
+ */
99
+ asClient() {
100
+ const client = new client_s3_1.S3Client({
101
+ region: 'auto',
102
+ endpoint: 'https://fake.s3.test',
103
+ forcePathStyle: true,
104
+ credentials: {
105
+ accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
106
+ secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
107
+ }
108
+ });
109
+ client.send = ((command) => this.send(command));
110
+ return client;
111
+ }
112
+ }
113
+ exports.FakeS3Client = FakeS3Client;
@@ -1,15 +1,71 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
1
2
  import type { StorageProvider } from '@orthacms/media-server';
2
- /** AWS S3 provider settings. */
3
+ /**
4
+ * Settings for an **S3-compatible** object store.
5
+ *
6
+ * Written endpoint-first rather than AWS-first on purpose: `endpoint` plus
7
+ * `forcePathStyle` is the whole difference between AWS S3, Cloudflare R2,
8
+ * MinIO, DigitalOcean Spaces, Backblaze B2, Wasabi, Scaleway, Hetzner, Supabase
9
+ * Storage and Tigris. One adapter, ten services — which is why this is one
10
+ * package and not ten.
11
+ */
3
12
  export interface S3StorageConfig {
13
+ /** Bucket every object lands in. */
4
14
  bucket: string;
5
- region: string;
15
+ /**
16
+ * Region. Required by AWS; most S3-compatible services ignore it but the
17
+ * signer still needs a value, so it defaults to `auto` (what R2 expects).
18
+ */
19
+ region?: string;
20
+ /**
21
+ * Full endpoint URL for a non-AWS service — e.g.
22
+ * `https://<account>.r2.cloudflarestorage.com`, or `http://localhost:9000`
23
+ * for MinIO. Omit for AWS S3 itself.
24
+ */
25
+ endpoint?: string;
26
+ /**
27
+ * Address objects as `<endpoint>/<bucket>/<key>` rather than
28
+ * `<bucket>.<endpoint>/<key>`. Required by MinIO and most self-hosted
29
+ * gateways; harmless for R2.
30
+ */
31
+ forcePathStyle?: boolean;
32
+ /**
33
+ * Explicit credentials. **Omit them** on a deployment with an instance
34
+ * role, IRSA or any other ambient credential source — the SDK's own
35
+ * provider chain is what should resolve those, and passing blanks here
36
+ * would shadow it with credentials that cannot sign.
37
+ */
38
+ credentials?: {
39
+ accessKeyId: string;
40
+ secretAccessKey: string;
41
+ sessionToken?: string;
42
+ };
43
+ /** Prefix every key with this, e.g. to share a bucket between environments. */
44
+ keyPrefix?: string;
45
+ /**
46
+ * An already-built client, for a deployment that needs custom middleware,
47
+ * a proxy agent or a retry strategy — and for tests, which drive this
48
+ * adapter against a stub rather than a network.
49
+ *
50
+ * When present, every other connection setting here is ignored: the client
51
+ * carries them.
52
+ */
53
+ client?: S3Client;
6
54
  }
7
55
  /**
8
- * Placeholder S3 {@link StorageProvider}. It proves the routing seam
9
- * end-to-end — the composition root can register it under a name and a resolver
10
- * can route to it without an AWS dependency. Every method throws until the
11
- * real adapter (put/get via the AWS SDK, signed `url`) lands. The signature
12
- * matches `createLocalStorageProvider`, so switching is a config change.
56
+ * The S3-compatible {@link StorageProvider}.
57
+ *
58
+ * Three things here are load-bearing and easy to lose in a refactor:
59
+ *
60
+ * 1. **`put` is all-or-nothing.** A multipart upload that fails leaves parts
61
+ * billed and invisible, and the key never reached a caller — so nothing can
62
+ * reclaim them. The `catch` aborts the upload, which is what removes them.
63
+ * 2. **size and checksum are measured here, not taken from the response.** The
64
+ * core persists both and the reclaim path trusts them; `ETag` is not a
65
+ * sha256 and is not even an MD5 for a multipart object.
66
+ * 3. **`get` rejects with `ObjectNotFoundError`**, mapped from the several
67
+ * shapes this family of services uses. A raw `NoSuchKey` escaping makes a
68
+ * missing blob a 500 where the route means to answer 404.
13
69
  */
14
70
  export declare function createS3StorageProvider(config: S3StorageConfig): StorageProvider;
15
71
  //# sourceMappingURL=s3-storage-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"s3-storage-provider.d.ts","sourceRoot":"","sources":["../../src/lib/s3-storage-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,wBAAwB,CAAC;AAE5E,gCAAgC;AAChC,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAYD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAEnC,MAAM,EAAE,eAAe,GACxB,eAAe,CAiBjB"}
1
+ {"version":3,"file":"s3-storage-provider.d.ts","sourceRoot":"","sources":["../../src/lib/s3-storage-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAIH,QAAQ,EAEX,MAAM,oBAAoB,CAAC;AAI5B,OAAO,KAAK,EAGR,eAAe,EAElB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC5B,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,WAAW,CAAC,EAAE;QACV,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CACrB;AAiCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,eAAe,GACxB,eAAe,CAsJjB"}
@@ -1,37 +1,174 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createS3StorageProvider = createS3StorageProvider;
4
- /** Raised by every method until the AWS adapter is implemented. */
5
- class S3NotImplementedError extends Error {
6
- constructor() {
7
- super('@orthacms/media-provider-s3 is a stub — the AWS S3 adapter is not implemented yet.');
8
- this.name = 'S3NotImplementedError';
9
- }
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_stream_1 = require("node:stream");
6
+ const client_s3_1 = require("@aws-sdk/client-s3");
7
+ const lib_storage_1 = require("@aws-sdk/lib-storage");
8
+ const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
9
+ const media_server_1 = require("@orthacms/media-server");
10
+ /** How many bytes one multipart part carries. The SDK's own minimum is 5 MB. */
11
+ const PART_SIZE = 5 * 1024 * 1024;
12
+ /** How many parts are in flight at once for one upload. */
13
+ const QUEUE_SIZE = 4;
14
+ /** Reduces a file name to one safe key segment. Mirrors the other providers. */
15
+ function sanitize(fileName) {
16
+ const cleaned = fileName.replace(/[^A-Za-z0-9_.-]+/g, '_');
17
+ // `.` and `..` survive the character class and are not names. S3 keys are
18
+ // opaque strings so neither would traverse anything — but a key that
19
+ // round-trips differently between two providers is a key the core cannot
20
+ // treat as opaque, and `..` in a key breaks most bucket browsers.
21
+ return cleaned === '.' || cleaned === '..' ? `_${cleaned}` : cleaned;
22
+ }
23
+ /** True for the several ways this family of services says "no such object". */
24
+ function isMissing(error) {
25
+ const candidate = error;
26
+ return (candidate?.name === 'NoSuchKey' ||
27
+ candidate?.name === 'NotFound' ||
28
+ candidate?.Code === 'NoSuchKey' ||
29
+ candidate?.$metadata?.httpStatusCode === 404);
10
30
  }
11
31
  /**
12
- * Placeholder S3 {@link StorageProvider}. It proves the routing seam
13
- * end-to-end — the composition root can register it under a name and a resolver
14
- * can route to it without an AWS dependency. Every method throws until the
15
- * real adapter (put/get via the AWS SDK, signed `url`) lands. The signature
16
- * matches `createLocalStorageProvider`, so switching is a config change.
32
+ * The S3-compatible {@link StorageProvider}.
33
+ *
34
+ * Three things here are load-bearing and easy to lose in a refactor:
35
+ *
36
+ * 1. **`put` is all-or-nothing.** A multipart upload that fails leaves parts
37
+ * billed and invisible, and the key never reached a caller — so nothing can
38
+ * reclaim them. The `catch` aborts the upload, which is what removes them.
39
+ * 2. **size and checksum are measured here, not taken from the response.** The
40
+ * core persists both and the reclaim path trusts them; `ETag` is not a
41
+ * sha256 and is not even an MD5 for a multipart object.
42
+ * 3. **`get` rejects with `ObjectNotFoundError`**, mapped from the several
43
+ * shapes this family of services uses. A raw `NoSuchKey` escaping makes a
44
+ * missing blob a 500 where the route means to answer 404.
17
45
  */
18
- function createS3StorageProvider(
19
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
- config) {
21
- // Implementations omit their params (the interface allows a narrower
22
- // signature) — there is nothing to act on until the AWS adapter lands.
46
+ function createS3StorageProvider(config) {
47
+ if (!config.bucket?.trim()) {
48
+ throw new Error('createS3StorageProvider requires a bucket. Set it in the host config, ' +
49
+ 'e.g. `storage: { bucket: process.env.MEDIA_S3_BUCKET }`.');
50
+ }
51
+ const clientConfig = {
52
+ region: config.region ?? 'auto',
53
+ ...(config.endpoint ? { endpoint: config.endpoint } : {}),
54
+ ...(config.forcePathStyle ? { forcePathStyle: true } : {}),
55
+ // Absent means "use the SDK's provider chain" — an instance role, IRSA,
56
+ // a shared config file. Passing an object with blank strings instead
57
+ // would shadow all of that with credentials that cannot sign.
58
+ ...(config.credentials ? { credentials: config.credentials } : {})
59
+ };
60
+ const client = config.client ?? new client_s3_1.S3Client(clientConfig);
61
+ const bucket = config.bucket;
62
+ const prefix = config.keyPrefix?.replace(/^\/+|\/+$/g, '');
63
+ const keyFor = (object) => [
64
+ prefix,
65
+ object.workspaceId,
66
+ object.assetId,
67
+ object.isVariant ? 'variants' : undefined,
68
+ sanitize(object.fileName)
69
+ ]
70
+ .filter(Boolean)
71
+ .join('/');
23
72
  return {
24
- put() {
25
- throw new S3NotImplementedError();
73
+ id: 's3',
74
+ capabilities: {
75
+ directUrl: true,
76
+ contentTypeMetadata: true,
77
+ streamingPut: true
78
+ },
79
+ async put(object) {
80
+ const storageKey = keyFor(object);
81
+ // Metered on the way through rather than buffered: an upload is
82
+ // bounded by `maxUploadBytes`, not by the heap, and `lib-storage`
83
+ // streams it in parts.
84
+ const hash = (0, node_crypto_1.createHash)('sha256');
85
+ let size = 0;
86
+ const meter = new node_stream_1.PassThrough();
87
+ meter.on('data', (chunk) => {
88
+ hash.update(chunk);
89
+ size += chunk.byteLength;
90
+ });
91
+ object.body.pipe(meter);
92
+ // A source that fails mid-stream must fail the upload, not stall
93
+ // it: `Upload` is waiting on a stream that will never end.
94
+ object.body.on('error', (error) => meter.destroy(error));
95
+ const upload = new lib_storage_1.Upload({
96
+ client,
97
+ partSize: PART_SIZE,
98
+ queueSize: QUEUE_SIZE,
99
+ params: {
100
+ Bucket: bucket,
101
+ Key: storageKey,
102
+ Body: meter,
103
+ // The object metadata is what makes `contentTypeMetadata`
104
+ // true here and false for a filesystem. It is the
105
+ // uploader's own claim, so the download route still
106
+ // hardens the response; this only keeps the object
107
+ // self-describing for anything reading the bucket directly.
108
+ ContentType: object.contentType
109
+ }
110
+ });
111
+ try {
112
+ await upload.done();
113
+ }
114
+ catch (error) {
115
+ // Without this the failed multipart upload's parts sit in the
116
+ // bucket, billed, invisible to `ListObjects`, and unreachable:
117
+ // the key was never returned, so nothing can reclaim them.
118
+ await upload.abort().catch(() => undefined);
119
+ throw error;
120
+ }
121
+ return { storageKey, size, checksum: hash.digest('hex') };
122
+ },
123
+ async get(storageKey) {
124
+ try {
125
+ const response = await client.send(new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: storageKey }));
126
+ if (!response.Body) {
127
+ throw new media_server_1.ObjectNotFoundError(storageKey);
128
+ }
129
+ return response.Body;
130
+ }
131
+ catch (error) {
132
+ if (isMissing(error)) {
133
+ throw new media_server_1.ObjectNotFoundError(storageKey, error);
134
+ }
135
+ throw error;
136
+ }
26
137
  },
27
- get() {
28
- throw new S3NotImplementedError();
138
+ async remove(storageKey) {
139
+ try {
140
+ await client.send(new client_s3_1.DeleteObjectCommand({ Bucket: bucket, Key: storageKey }));
141
+ }
142
+ catch (error) {
143
+ // S3 answers 204 for a key that was never there, so this is
144
+ // already idempotent — but a gateway that answers 404 instead
145
+ // must not turn a best-effort, post-commit reclaim into an
146
+ // error nobody can act on.
147
+ if (!isMissing(error))
148
+ throw error;
149
+ }
29
150
  },
30
- remove() {
31
- throw new S3NotImplementedError();
151
+ async directUrl(storageKey, options) {
152
+ // The disposition and content type are **pinned on the URL**, not
153
+ // left to the object's metadata. A redirect discards the app's own
154
+ // `Content-Disposition`, `nosniff` and CSP, and `mime_type` is the
155
+ // uploader's unverified claim — so an uploaded `.html` served
156
+ // inline from the bucket would be stored XSS. These two response
157
+ // overrides are the whole reason this provider may declare
158
+ // `capabilities.directUrl`.
159
+ const fileName = options.fileName.replace(/"/g, '');
160
+ return (0, s3_request_presigner_1.getSignedUrl)(client, new client_s3_1.GetObjectCommand({
161
+ Bucket: bucket,
162
+ Key: storageKey,
163
+ ResponseContentDisposition: `${options.disposition}; filename="${fileName}"`,
164
+ ResponseContentType: options.contentType
165
+ }), { expiresIn: options.expiresInSeconds });
32
166
  },
33
- url() {
34
- throw new S3NotImplementedError();
167
+ async verify() {
168
+ // One HEAD at boot. A wrong bucket, a dead endpoint or an expired
169
+ // key fails the start instead of the first upload, hours later,
170
+ // with nothing in the message naming the cause.
171
+ await client.send(new client_s3_1.HeadBucketCommand({ Bucket: bucket }));
35
172
  }
36
173
  };
37
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orthacms/media-provider-s3",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "@orthacms/media-provider-s3 — part of Ortha CMS.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/media/provider-s3",
@@ -25,7 +25,10 @@
25
25
  "dist"
26
26
  ],
27
27
  "dependencies": {
28
- "@orthacms/media-server": "^0.3.0",
28
+ "@aws-sdk/client-s3": "^3.716.0",
29
+ "@aws-sdk/lib-storage": "^3.716.0",
30
+ "@aws-sdk/s3-request-presigner": "^3.716.0",
31
+ "@orthacms/media-server": "^0.4.0",
29
32
  "tslib": "^2.3.0"
30
33
  },
31
34
  "publishConfig": {