9.2 KiB
OpenSpec Change: enterprise-storage-s3
- Change ID: enterprise-storage-s3
- Package:
@suaveplan/storage-s3 - Category: universal
- Target path:
universal/storage/storage-s3 - Status: PROPOSED
- Date: 2026-04-24
- Reference spec:
/mnt/s/web/suaveplan/enterprise/storage-s3.md
Summary
Deliver @suaveplan/storage-s3 on the genesis branch from scratch at enterprise quality. The package provides an S3-compatible object storage implementation that extends @suaveplan/storage's AbstractStorage base class. It implements all StorageAdapter methods (get, set, delete, list, copy, move, exists, metadata) backed by @aws-sdk/client-s3, registers itself with StorageFactory as the s3 provider, and exposes a createS3 factory function. Genesis adds structured errors via @suaveplan/error, Zod option schemas, OTel instrumentation, retry integration via @suaveplan/retry, AbortSignal support throughout, presigned URL generation, multipart upload for large objects, credential validation, property-based tests (fast-check), and a mitata benchmark suite. The main-branch implementation uses @aws-sdk/client-s3 directly; genesis adds AWS SDK v3 presigned URLs via @aws-sdk/s3-request-presigner.
Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/storage (workspace:^) |
AbstractStorage, StorageFactory, StorageError, all shared types |
@suaveplan/error (workspace:^) |
Base class for S3StorageError; defineCodes for S3.* codes |
@suaveplan/types (workspace:^) |
LoggerLike, Clock |
@suaveplan/retry (workspace:^) |
withRetry, RetryStrategyOptions — wired on all S3 API calls |
@aws-sdk/client-s3 (catalog:storage) |
S3 commands (Get, Put, Delete, List, Head, Copy) |
@aws-sdk/s3-request-presigner (catalog:storage) |
getSignedUrl for presigned URL generation |
zod (catalog:forms) |
Zod schemas for S3Options and PresignOptions |
@opentelemetry/api (catalog:observability) |
Spans + counters on every operation |
tslib (catalog:build) |
Runtime helpers |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check, mitata (all catalog:testing), @suaveplan/testing workspace:^.
Preserved public API surface (from main branch)
Core class
class S3 extends AbstractStorage {
constructor(options: S3Options)
// StorageAdapter methods (all from AbstractStorage contract):
get(key: string, options?: StorageOptions): Promise<StorageResult>
set(key: string, value: Uint8Array | string, options?: StorageOptions): Promise<void>
delete(key: string): Promise<void>
list(prefix?: string, options?: StorageListOptions): Promise<StorageListResult>
exists(key: string): Promise<boolean>
metadata(key: string): Promise<StorageMetadata>
copy(source: string, destination: string): Promise<void>
move(source: string, destination: string): Promise<void>
}
Options
interface S3Options {
bucket: string
region: string
accessKeyId?: string
secretAccessKey?: string
endpoint?: string
keyPrefix?: string
}
Factory registration
The package imports StorageFactory from @suaveplan/storage and calls StorageFactory.register('s3', S3) as a side-effect on package load (via src/registry.ts).
Error codes (genesis addition, not in main)
type S3ErrorCode =
| 'S3_CREDENTIALS_MISSING'
| 'S3_CREDENTIALS_INVALID'
| 'S3_OBJECT_NOT_FOUND'
| 'S3_BUCKET_NOT_FOUND'
| 'S3_UPLOAD_FAILED'
| 'S3_DELETE_FAILED'
| 'S3_LIST_FAILED'
| 'S3_PRESIGN_FAILED'
| 'S3_MULTIPART_FAILED'
| 'S3_REGION_MISMATCH'
New additions for genesis
1. Structured S3StorageError
S3StorageError extends SuaveplanError from @suaveplan/error carrying an S3ErrorCode discriminant. All internal throw sites use named factory helpers. The StorageError from @suaveplan/storage is preserved for backward compatibility — S3StorageError is emitted for S3-specific failures while StorageError wraps generic store-layer failures.
2. Zod option schemas
S3OptionsSchema— validates all fields including credential format guards (conditional onisAwsS3detection).PresignOptionsSchema— for the newpresign(key, options)method.MultipartUploadOptionsSchema— forsetMultipart(key, parts, options).
3. Retry integration
S3Options gains an optional retry?: RetryStrategyOptions. When provided, every S3 API call is wrapped with @suaveplan/retry's withRetry. Default: no retry (caller opt-in). Retryable S3 errors (5xx, SlowDown, RequestTimeout) are automatically detected from the AWS SDK error code.
4. AbortSignal support
All async methods accept an optional signal?: AbortSignal passed through to the AWS SDK AbortController interop. Aborted requests throw S3StorageError(S3_UPLOAD_FAILED) with retryable: false.
5. Presigned URL generation
interface PresignOptions { expiresIn?: number; method?: 'GET' | 'PUT' }
presign(key: string, options?: PresignOptions): Promise<string>
Uses @aws-sdk/s3-request-presigner's getSignedUrl. Throws S3StorageError(S3_PRESIGN_FAILED) on failure.
6. Multipart upload
interface MultipartUploadOptions { partSize?: number; concurrency?: number; signal?: AbortSignal }
setMultipart(key: string, stream: ReadableStream<Uint8Array>, options?: MultipartUploadOptions): Promise<void>
Uses CreateMultipartUploadCommand + UploadPartCommand + CompleteMultipartUploadCommand. Aborts the multipart session on error or signal. Default part size: 5 MiB (AWS minimum). Throws S3StorageError(S3_MULTIPART_FAILED) on failure.
7. OTel instrumentation
| Operation | Signal |
|---|---|
get |
span s3.get, histogram s3.get_duration_ms, counter s3.get_total |
set |
span s3.set, histogram s3.set_duration_ms, counter s3.set_total, attribute s3.object_size |
delete |
span s3.delete, counter s3.delete_total |
list |
span s3.list, counter s3.list_total, attribute s3.prefix |
copy / move |
span s3.copy / s3.move |
presign |
span s3.presign, attribute s3.expires_in |
setMultipart |
span s3.multipart, attribute s3.part_count |
8. Logger injection
S3Options gains an optional logger?: LoggerLike. When provided, each operation emits structured entries at debug level with key, bucket, duration.
9. Credential format guard
AWS S3 credential validation (AKIA prefix + 20 chars, secret key length ≥ 40) is preserved from main. For S3-compatible endpoints (non-amazonaws.com), validation is skipped. This logic now throws S3StorageError with appropriate code instead of the main-branch StorageError.
10. Property-based tests (fast-check)
- Key prefix normalisation: for arbitrary key strings and prefix strings, assert that the final S3 key is always
prefix + keywith no double slashes. - Credential detection: for arbitrary endpoint URLs, assert that
isAwsS3(endpoint)returns true iff the hostname containsamazonaws.com. - List pagination: for N objects spread across M pages, assert that
list()returns all N keys.
11. Benchmark suite
get_hot: 100get()calls on pre-existing small objects (requiresGARAGE_ENDPOINTenv; skip otherwise).set_1mb: upload a 1 MiB buffer (requires env; skip otherwise).list_1000: list 1000 objects in a bucket (requires env; skip otherwise).
File layout
universal/storage/storage-s3/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts # Public barrel
error-codes.ts # S3.* codes + registerS3ErrorCodes
constants/
index.ts # DEFAULT_PART_SIZE, DEFAULT_PRESIGN_EXPIRES
types/
s3.types.ts # S3Options, PresignOptions, MultipartUploadOptions
s3.types.md
schemas.ts # Zod schemas
schemas.md
s3/
s3/
s3.ts # S3 class
s3.md
s3.test.ts
index.ts
registry/
registry/
registry.ts # StorageFactory.register('s3', S3) side-effect
registry.md
index.ts
utils/
credential-validator.ts # isAwsS3, validateCredentials
credential-validator.md
credential-validator.test.ts
__benchmarks__/
s3.bench.ts
tests/
integration.test.ts # Real Garage/MinIO (GARAGE_ENDPOINT guard)
property.test.ts # fast-check property tests
Impact
Downstream packages: @suaveplan/drive uses @suaveplan/storage (not storage-s3 directly); any package needing S3 object storage will add @suaveplan/storage-s3 as an optional dep.
Breaking changes from main-branch API: none. The S3 class constructor signature is unchanged; new optional fields (retry, signal, logger) are additive; presign and setMultipart are new methods.
Workspace dep prerequisites: @suaveplan/storage, @suaveplan/error, @suaveplan/types, @suaveplan/retry must be published to Verdaccio before this package can be built or tested.