Files
2026-05-27 23:29:53 +01:00

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 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

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 + 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

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.