@superfunctions/storage-s3 0.1.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,10 @@
1
+ import type { StorageAdapter } from '@superfunctions/storage';
2
+ export interface S3StorageConfig {
3
+ bucket: string;
4
+ region: string;
5
+ accessKeyId?: string;
6
+ secretAccessKey?: string;
7
+ endpoint?: string;
8
+ forcePathStyle?: boolean;
9
+ }
10
+ export declare function createS3StorageAdapter(config: S3StorageConfig): StorageAdapter;
@@ -0,0 +1,30 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { createS3CompatibleStorageAdapter } from '@superfunctions/storage/internal/s3-compatible';
3
+ export function createS3StorageAdapter(config) {
4
+ const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = config;
5
+ const hasAccessKeyId = accessKeyId !== undefined;
6
+ const hasSecretAccessKey = secretAccessKey !== undefined;
7
+ let credentials;
8
+ if (hasAccessKeyId !== hasSecretAccessKey) {
9
+ throw new Error('S3 storage config requires both accessKeyId and secretAccessKey when using static credentials');
10
+ }
11
+ if (hasAccessKeyId && hasSecretAccessKey) {
12
+ if (accessKeyId === '' || secretAccessKey === '') {
13
+ throw new Error('S3 storage config requires non-empty accessKeyId and secretAccessKey when using static credentials');
14
+ }
15
+ credentials = { accessKeyId, secretAccessKey };
16
+ }
17
+ const client = new S3Client({
18
+ region,
19
+ endpoint,
20
+ forcePathStyle,
21
+ credentials,
22
+ });
23
+ return createS3CompatibleStorageAdapter({
24
+ name: 's3',
25
+ bucket,
26
+ client,
27
+ statNotFoundErrorNames: ['NotFound'],
28
+ downloadNotFoundErrorNames: ['NoSuchKey'],
29
+ });
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createS3StorageAdapter } from './adapter.js';
3
+ import { runConformanceTests } from '@superfunctions/storage';
4
+ describe('storage-s3 adapter', () => {
5
+ describe('conformance', () => {
6
+ it('should pass conformance tests (method existence)', async () => {
7
+ const adapter = createS3StorageAdapter({
8
+ bucket: 'test-bucket',
9
+ region: 'us-east-1',
10
+ });
11
+ const result = await runConformanceTests(adapter);
12
+ expect(result.ok).toBe(true);
13
+ });
14
+ it('should have correct capabilities', () => {
15
+ const adapter = createS3StorageAdapter({
16
+ bucket: 'test-bucket',
17
+ region: 'us-east-1',
18
+ });
19
+ expect(adapter.capabilities).toEqual({
20
+ signedUploadUrls: true,
21
+ signedDownloadUrls: true,
22
+ multipart: true,
23
+ proxyStreamingUpload: false,
24
+ proxyStreamingDownload: true,
25
+ });
26
+ });
27
+ });
28
+ describe('adapter creation', () => {
29
+ it('should create adapter with minimal config', () => {
30
+ const adapter = createS3StorageAdapter({
31
+ bucket: 'my-bucket',
32
+ region: 'eu-west-1',
33
+ });
34
+ expect(adapter.name).toBe('s3');
35
+ });
36
+ it('should create adapter with full config', () => {
37
+ const adapter = createS3StorageAdapter({
38
+ bucket: 'my-bucket',
39
+ region: 'us-east-1',
40
+ accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
41
+ secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
42
+ endpoint: 'http://localhost:9000',
43
+ forcePathStyle: true,
44
+ });
45
+ expect(adapter.name).toBe('s3');
46
+ });
47
+ it('requires both static credential fields when either is provided', () => {
48
+ expect(() => createS3StorageAdapter({
49
+ bucket: 'my-bucket',
50
+ region: 'us-east-1',
51
+ accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
52
+ })).toThrow('S3 storage config requires both accessKeyId and secretAccessKey when using static credentials');
53
+ });
54
+ it('rejects empty static credentials instead of silently falling back', () => {
55
+ expect(() => createS3StorageAdapter({
56
+ bucket: 'my-bucket',
57
+ region: 'us-east-1',
58
+ accessKeyId: '',
59
+ secretAccessKey: '',
60
+ })).toThrow('S3 storage config requires non-empty accessKeyId and secretAccessKey when using static credentials');
61
+ });
62
+ });
63
+ describe('multipart methods', () => {
64
+ it('should have all multipart methods', () => {
65
+ const adapter = createS3StorageAdapter({
66
+ bucket: 'test-bucket',
67
+ region: 'us-east-1',
68
+ });
69
+ expect(adapter.createMultipartUpload).toBeDefined();
70
+ expect(adapter.signMultipartUploadPartUrl).toBeDefined();
71
+ expect(adapter.completeMultipartUpload).toBeDefined();
72
+ expect(adapter.abortMultipartUpload).toBeDefined();
73
+ });
74
+ });
75
+ describe('signed URL methods', () => {
76
+ it('should have signed URL methods', () => {
77
+ const adapter = createS3StorageAdapter({
78
+ bucket: 'test-bucket',
79
+ region: 'us-east-1',
80
+ });
81
+ expect(adapter.signUploadUrl).toBeDefined();
82
+ expect(adapter.signDownloadUrl).toBeDefined();
83
+ });
84
+ });
85
+ describe('completeMultipartUpload validation', () => {
86
+ it('should reject empty parts array', async () => {
87
+ const adapter = createS3StorageAdapter({
88
+ bucket: 'test-bucket',
89
+ region: 'us-east-1',
90
+ });
91
+ await expect(adapter.completeMultipartUpload({
92
+ key: 'test-key',
93
+ uploadId: 'test-upload-id',
94
+ parts: [],
95
+ })).rejects.toMatchObject({ code: 'STORAGE_MULTIPART_INVALID' });
96
+ });
97
+ });
98
+ });
@@ -0,0 +1,2 @@
1
+ export { createS3StorageAdapter, createS3StorageAdapter as createS3Storage } from './adapter.js';
2
+ export type { S3StorageConfig } from './adapter.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createS3StorageAdapter, createS3StorageAdapter as createS3Storage } from './adapter.js';
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@superfunctions/storage-s3",
3
+ "version": "0.1.0",
4
+ "description": "S3 storage adapter for @superfunctions/storage",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "test": "vitest",
18
+ "typecheck": "tsc --noEmit",
19
+ "clean": "rm -rf dist"
20
+ },
21
+ "keywords": ["storage", "s3", "aws", "superfunctions"],
22
+ "author": "21n",
23
+ "license": "MIT",
24
+ "bugs": { "url": "https://github.com/21nCo/super-functions/issues" },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/21nCo/super-functions.git",
28
+ "directory": "packages/storage-s3"
29
+ },
30
+ "publishConfig": { "access": "public" },
31
+ "dependencies": {
32
+ "@aws-sdk/client-s3": "^3.600.0",
33
+ "@aws-sdk/s3-request-presigner": "^3.600.0",
34
+ "@superfunctions/storage": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "typescript": "^5.6.0",
39
+ "vitest": "^3.2.4"
40
+ }
41
+ }