9.1 KiB
9.1 KiB
Tasks: enterprise-storage-s3
Change ID: enterprise-storage-s3
Package: @suaveplan/storage-s3
Target path: universal/storage/storage-s3
Prerequisite packages on Verdaccio: @suaveplan/storage, @suaveplan/error, @suaveplan/types, @suaveplan/retry
1. Scaffold
- Create directory
universal/storage/storage-s3/withsrc/,tests/,src/__benchmarks__/, and all feature subdirectories per the layout inproposal.md. - Write
package.jsonat version0.1.0with:"type": "module","sideEffects": falseexportsmap: top-level"."barrel plus subpaths./s3/s3,./types/s3.types,./types/schemasdependencies:@suaveplan/storage workspace:^,@suaveplan/error workspace:^,@suaveplan/types workspace:^,@suaveplan/retry workspace:^,@aws-sdk/client-s3 catalog:storage,@aws-sdk/s3-request-presigner catalog:storage,zod catalog:forms,@opentelemetry/api catalog:observability,tslib catalog:builddevDependencies:vitest catalog:testing,@vitest/coverage-istanbul catalog:testing,fast-check catalog:testing,mitata catalog:testing,@suaveplan/testing workspace:^
- Write
tsconfig.jsonextending@suaveplan/typescript-config/library.json. - Write
vitest.config.tsusingcreateTestConfig()from@suaveplan/testing/config(pool:forks). - Add
lint,typecheck,test,test:coverage,build,docs:check,benchscripts topackage.json.
2. Types + errors
2a. S3 types (src/types/s3.types.ts + src/types/s3.types.md)
- Define
S3Options:bucket,region,accessKeyId?,secretAccessKey?,endpoint?,keyPrefix?— preserved from main. - Add genesis-only optional fields:
retry?: RetryStrategyOptions,logger?: LoggerLike,signal?: AbortSignal(default for all ops). - Define
PresignOptions:expiresIn?: number,method?: 'GET' | 'PUT'. - Define
MultipartUploadOptions:partSize?: number,concurrency?: number,signal?: AbortSignal. - Write
src/types/s3.types.md(≥200 words, API reference, examples).
2b. Error codes (src/error-codes.ts)
- Import
CodeMetadata,ErrorCodes,SuaveplanErrorfrom@suaveplan/error. - Define
S3ErrorCodeunion — ten codes: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. - Define
S3_ERROR_METADATAwithhttpStatus,severity,retryable,category,descriptionper code. - Export
registerS3ErrorCodes(). Idempotent; called eagerly at module scope. - Export
S3StorageErrorextendingSuaveplanErrorcarryingS3ErrorCodediscriminant.
2c. Zod schemas (src/types/schemas.ts + src/types/schemas.md)
- Write
S3OptionsSchema,PresignOptionsSchema,MultipartUploadOptionsSchema. - Write
src/types/schemas.md(≥200 words).
3. Constants (src/constants/index.ts)
- Export
DEFAULT_PRESIGN_EXPIRES_SECONDS = 3600,DEFAULT_MULTIPART_PART_SIZE = 5 * 1024 * 1024,DEFAULT_MULTIPART_CONCURRENCY = 4.
4. Credential validator (src/utils/credential-validator.ts)
- Implement
isAwsS3(endpoint?: string): boolean— returnstrueif endpoint is undefined or hostname containsamazonaws.com. - Implement
validateCredentials(options: S3Options): void:- Throws
S3StorageError(S3_CREDENTIALS_MISSING)ifaccessKeyIdorsecretAccessKeyabsent (neither in options nor env). - Throws
S3StorageError(S3_CREDENTIALS_INVALID)for invalid AWS AKIA key format whenisAwsS3(options.endpoint)is true. - No validation for S3-compatible endpoints.
- Throws
- Write
credential-validator.test.ts: valid AWS creds, invalid format, missing creds, S3-compatible endpoint skips validation. - Write
credential-validator.md(≥200 words).
5. S3 class (src/s3/s3/)
- Implement
S3 extends AbstractStorage(import from@suaveplan/storage):- Constructor calls
validateCredentials(options), buildsS3Clientwith{ region, credentials: { accessKeyId, secretAccessKey }, endpoint }. - Internal
_resolveKey(key): string— appliesoptions.keyPrefixstripping double-slashes. get(key, options?):- Sends
GetObjectCommand; reads response body asUint8Array. - Throws
S3StorageError(S3_OBJECT_NOT_FOUND)onNoSuchKey. - OTel span + counter. Retry wrapper if configured.
- Sends
set(key, value, options?):- Sends
PutObjectCommand. - Throws
S3StorageError(S3_UPLOAD_FAILED)on error. - OTel span + histogram (object size attribute).
- Sends
delete(key):- Sends
DeleteObjectCommand. - Throws
S3StorageError(S3_DELETE_FAILED)on error.
- Sends
list(prefix?, options?):- Sends
ListObjectsV2Commandwith pagination (handlesNextContinuationToken). - Throws
S3StorageError(S3_LIST_FAILED)on error.
- Sends
exists(key): sendsHeadObjectCommand; returnstrueon 200,falseonNoSuchKey.metadata(key): parsesHeadObjectCommandresponse intoStorageMetadata.copy(source, destination): sendsCopyObjectCommand.move(source, destination):copythendelete.presign(key, options?): usesgetSignedUrlfrom@aws-sdk/s3-request-presigner. ThrowsS3StorageError(S3_PRESIGN_FAILED).setMultipart(key, stream, options?):CreateMultipartUploadCommand→ upload parts with concurrency cap →CompleteMultipartUploadCommand.- On any error:
AbortMultipartUploadCommandthen rethrow asS3StorageError(S3_MULTIPART_FAILED). - Respects
options.signal(abort mid-upload).
- All methods accept per-call
signal?: AbortSignalthat overrides the instance-level default. - Log via injected
loggeratdebuglevel.
- Constructor calls
- Write
s3.test.ts(unit: inject a mockS3Clientvia constructor option; no vi.mock — DI only). Cover: get hit, get miss (404), set, delete, list with pagination, exists true/false, copy, move, presign, credential validation failure. - Write
s3.md(≥200 words, both basic + advanced example with retry config).
6. Registry (src/registry/registry/)
registry.ts— importsS3and callsStorageFactory.register('s3', S3)as a side-effect on import.- Write
registry.mdexplaining the side-effect and tree-shaking implications.
7. Public barrel (src/index.ts)
- Re-export:
S3,S3StorageError,S3OptionsSchema,PresignOptionsSchema,MultipartUploadOptionsSchema, all types, error-codes, constants. - Import
./registry/registryto trigger provider registration.
8. Property-based tests (tests/property.test.ts)
Use fast-check. All timeouts: { timeout: 30000 }.
- Key prefix normalisation: for arbitrary key and prefix strings (alphanum +
/), assertresolveKey(prefix, key)never produces double slashes and always starts with prefix. isAwsS3detection: for arbitrary endpoint strings, assertisAwsS3returns true iff hostname containsamazonaws.com.- List pagination assembly: given M pages of N/M keys each, assert the assembled list equals the concatenation of all pages in order.
9. Integration tests (tests/integration.test.ts)
- Gate all tests behind
process.env.GARAGE_ENDPOINTskip guard. - Test: put → get → exists → metadata → list → copy → move → delete against a real Garage S3-compatible server.
- Test: multipart upload of a 15 MiB buffer (3 × 5 MiB parts).
- Test: presigned GET URL is accessible without credentials.
10. Benchmarks (src/__benchmarks__/s3.bench.ts)
Use mitata. Gate IO benchmarks behind SKIP_S3_BENCHMARKS env flag.
get_hot_100(conditional): 100get()calls on a pre-existing 1 KiB object.set_1mb(conditional): upload a 1 MiB buffer.list_1000(conditional): list 1000 objects.credential_validate_1k(pure): 1000 credential validation calls.
11. Docs
src/types/s3.types.mdsrc/types/schemas.mdsrc/s3/s3/s3.mdsrc/registry/registry/registry.mdsrc/utils/credential-validator.md- Root
universal/storage/storage-s3/README.md.
12. Verify and publish
- Run
bunx turbo lint --filter=@suaveplan/storage-s3— zero errors. - Run
bunx turbo typecheck --filter=@suaveplan/storage-s3— zero errors. - Run
bunx turbo test --filter=@suaveplan/storage-s3— all tests pass. - Run
bunx turbo test:coverage --filter=@suaveplan/storage-s3— 100% on all touched modules. - Run
bun run docs:check --filter=@suaveplan/storage-s3— zero errors. - Confirm no
TODO,FIXME,@deprecated, untypedany,ts-ignore, or.skipinsrc/ortests/. - Confirm
"sideEffects": falseinpackage.json. - Run
bun scripts/publish.ts --package @suaveplan/storage-s3. - Update
CLAUDE.mdpackage listing to include@suaveplan/storage-s3. - Archive: move
openspec/changes/enterprise-storage-s3/toopenspec/archive/2026-04-24-enterprise-storage-s3/and fold spec delta intoopenspec/specs/storage/spec.md.