# 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 ```ts class S3 extends AbstractStorage { constructor(options: S3Options) // StorageAdapter methods (all from AbstractStorage contract): get(key: string, options?: StorageOptions): Promise set(key: string, value: Uint8Array | string, options?: StorageOptions): Promise delete(key: string): Promise list(prefix?: string, options?: StorageListOptions): Promise exists(key: string): Promise metadata(key: string): Promise copy(source: string, destination: string): Promise move(source: string, destination: string): Promise } ``` ### Options ```ts 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) ```ts 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 on `isAwsS3` detection). - `PresignOptionsSchema` — for the new `presign(key, options)` method. - `MultipartUploadOptionsSchema` — for `setMultipart(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 ```ts interface PresignOptions { expiresIn?: number; method?: 'GET' | 'PUT' } presign(key: string, options?: PresignOptions): Promise ``` Uses `@aws-sdk/s3-request-presigner`'s `getSignedUrl`. Throws `S3StorageError(S3_PRESIGN_FAILED)` on failure. ### 6. Multipart upload ```ts interface MultipartUploadOptions { partSize?: number; concurrency?: number; signal?: AbortSignal } setMultipart(key: string, stream: ReadableStream, options?: MultipartUploadOptions): Promise ``` 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 + key` with no double slashes. - **Credential detection**: for arbitrary endpoint URLs, assert that `isAwsS3(endpoint)` returns true iff the hostname contains `amazonaws.com`. - **List pagination**: for N objects spread across M pages, assert that `list()` returns all N keys. ### 11. Benchmark suite - `get_hot`: 100 `get()` calls on pre-existing small objects (requires `GARAGE_ENDPOINT` env; 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 ```text 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.