feat(media): S3/MinIO storage adapter (StoragePort)

Drop-in S3-compatible backend for object storage (media + email attachments) —
the swap the StoragePort seam was designed for. MinIO/self-hosted/R2/Supabase all
work via endpoint + path-style.

- S3Storage implements StoragePort (put/get/remove); sha256 computed locally on put
  (matches LocalDiskStorage); a missing object reads back as null, not an error.
- Env-driven binding in MediaModule: IIOS_S3_BUCKET + keys set → S3Storage, else
  LocalDiskStorage. forcePathStyle defaults true (MinIO); endpoint omitted → AWS.
- S3 client is injectable so tests run with no live server.

6 unit tests (config parse, put size/sha, get round-trip, NoSuchKey→null, remove).
Full suite 300/300, boundary + build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 14:26:53 +05:30
parent d28c873d40
commit 0c6650f3c6
6 changed files with 479 additions and 4 deletions
+302
View File
@@ -328,6 +328,9 @@ importers:
packages/iios-service:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.1090.0
version: 3.1090.0
'@insignia/iios-adapter-sdk':
specifier: workspace:*
version: link:../iios-adapter-sdk
@@ -487,6 +490,78 @@ packages:
resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==}
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
'@aws-sdk/checksums@3.1000.18':
resolution: {integrity: sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/client-s3@3.1090.0':
resolution: {integrity: sha512-R6GX9cd1jljwzZ8xFmgAI/hHCuX1MobIKBdsymv7WL9SENvO9Vgz9KOR6avTnu0Ao+w1LmxnTe+jqmZXEn7Q/Q==}
engines: {node: '>=20.0.0'}
'@aws-sdk/core@3.975.3':
resolution: {integrity: sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-env@3.972.59':
resolution: {integrity: sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-http@3.972.61':
resolution: {integrity: sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-ini@3.973.4':
resolution: {integrity: sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-login@3.972.66':
resolution: {integrity: sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-node@3.972.70':
resolution: {integrity: sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-process@3.972.59':
resolution: {integrity: sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-sso@3.973.3':
resolution: {integrity: sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-web-identity@3.972.65':
resolution: {integrity: sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-sdk-s3@3.972.64':
resolution: {integrity: sha512-RBi43anhDBUv+HCfxCOXwGOE7GmT4n7ChV04Mwr22RhXTNcamW/iWnJlOotDPCZSrJ4dEvhZSiWWQMwLX+ZhFA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/nested-clients@3.997.33':
resolution: {integrity: sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/signature-v4-multi-region@3.996.41':
resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==}
engines: {node: '>=20.0.0'}
'@aws-sdk/token-providers@3.1088.0':
resolution: {integrity: sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/types@3.974.2':
resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/xml-builder@3.972.36':
resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==}
engines: {node: '>=20.0.0'}
'@aws/lambda-invoke-store@0.3.0':
resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
engines: {node: '>=18.0.0'}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
@@ -1450,6 +1525,30 @@ packages:
cpu: [x64]
os: [win32]
'@smithy/core@3.29.5':
resolution: {integrity: sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ==}
engines: {node: '>=18.0.0'}
'@smithy/credential-provider-imds@4.4.10':
resolution: {integrity: sha512-MJenAe4OKRZUo1LdYYFDCsSHxaHvInIU/z52GsheO9vl1/VSySVCr0zkyKD6TFiGkSUaWGxvKZ/70OvgUZR5HQ==}
engines: {node: '>=18.0.0'}
'@smithy/fetch-http-handler@5.6.7':
resolution: {integrity: sha512-3zpg8yqqyXzoK2TsRDdkqVOj2RDBFfLXwCczOZ5c7TWB4eiaebfSCsbMjDPYB3PJ9ihV62QaeadZ+wLadZtNGA==}
engines: {node: '>=18.0.0'}
'@smithy/node-http-handler@4.9.7':
resolution: {integrity: sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA==}
engines: {node: '>=18.0.0'}
'@smithy/signature-v4@5.6.6':
resolution: {integrity: sha512-efP6DN3UTFrzIsGO42/xcabv8jU7+9nwEdphFUH7yL0k010ERyAWaO41KFQIDLcFZLZ8xzIQr4wplFxNzslSGQ==}
engines: {node: '>=18.0.0'}
'@smithy/types@4.16.1':
resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==}
engines: {node: '>=18.0.0'}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -1766,6 +1865,9 @@ packages:
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
engines: {node: '>=18'}
bowser@2.14.1:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
brace-expansion@1.1.15:
resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==}
@@ -3362,6 +3464,171 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@aws-sdk/checksums@3.1000.18':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/client-s3@3.1090.0':
dependencies:
'@aws-sdk/checksums': 3.1000.18
'@aws-sdk/core': 3.975.3
'@aws-sdk/credential-provider-node': 3.972.70
'@aws-sdk/middleware-sdk-s3': 3.972.64
'@aws-sdk/signature-v4-multi-region': 3.996.41
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/fetch-http-handler': 5.6.7
'@smithy/node-http-handler': 4.9.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/core@3.975.3':
dependencies:
'@aws-sdk/types': 3.974.2
'@aws-sdk/xml-builder': 3.972.36
'@aws/lambda-invoke-store': 0.3.0
'@smithy/core': 3.29.5
'@smithy/signature-v4': 5.6.6
'@smithy/types': 4.16.1
bowser: 2.14.1
tslib: 2.8.1
'@aws-sdk/credential-provider-env@3.972.59':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-http@3.972.61':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/fetch-http-handler': 5.6.7
'@smithy/node-http-handler': 4.9.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-ini@3.973.4':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/credential-provider-env': 3.972.59
'@aws-sdk/credential-provider-http': 3.972.61
'@aws-sdk/credential-provider-login': 3.972.66
'@aws-sdk/credential-provider-process': 3.972.59
'@aws-sdk/credential-provider-sso': 3.973.3
'@aws-sdk/credential-provider-web-identity': 3.972.65
'@aws-sdk/nested-clients': 3.997.33
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/credential-provider-imds': 4.4.10
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-login@3.972.66':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/nested-clients': 3.997.33
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-node@3.972.70':
dependencies:
'@aws-sdk/credential-provider-env': 3.972.59
'@aws-sdk/credential-provider-http': 3.972.61
'@aws-sdk/credential-provider-ini': 3.973.4
'@aws-sdk/credential-provider-process': 3.972.59
'@aws-sdk/credential-provider-sso': 3.973.3
'@aws-sdk/credential-provider-web-identity': 3.972.65
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/credential-provider-imds': 4.4.10
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-process@3.972.59':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-sso@3.973.3':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/nested-clients': 3.997.33
'@aws-sdk/token-providers': 3.1088.0
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-web-identity@3.972.65':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/nested-clients': 3.997.33
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/middleware-sdk-s3@3.972.64':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/signature-v4-multi-region': 3.996.41
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/nested-clients@3.997.33':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/signature-v4-multi-region': 3.996.41
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/fetch-http-handler': 5.6.7
'@smithy/node-http-handler': 4.9.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/signature-v4-multi-region@3.996.41':
dependencies:
'@aws-sdk/types': 3.974.2
'@smithy/signature-v4': 5.6.6
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/token-providers@3.1088.0':
dependencies:
'@aws-sdk/core': 3.975.3
'@aws-sdk/nested-clients': 3.997.33
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/types@3.974.2':
dependencies:
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/xml-builder@3.972.36':
dependencies:
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws/lambda-invoke-store@0.3.0': {}
'@babel/code-frame@7.29.7':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -4105,6 +4372,39 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.62.2':
optional: true
'@smithy/core@3.29.5':
dependencies:
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/credential-provider-imds@4.4.10':
dependencies:
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/fetch-http-handler@5.6.7':
dependencies:
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/node-http-handler@4.9.7':
dependencies:
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/signature-v4@5.6.6':
dependencies:
'@smithy/core': 3.29.5
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/types@4.16.1':
dependencies:
tslib: 2.8.1
'@socket.io/component-emitter@3.1.2': {}
'@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.8)':
@@ -4492,6 +4792,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
bowser@2.14.1: {}
brace-expansion@1.1.15:
dependencies:
balanced-match: 1.0.2