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

13 KiB

Tasks: complete-storage-s3

0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Foundation — constants, error codes, types

  • 1.1 Extend src/constants/index.ts with DEFAULT_MULTIPART_THRESHOLD = 16 MiB, DEFAULT_MULTIPART_CONCURRENCY = 4, MIN_PART_SIZE = 5 MiB, MAX_PART_SIZE = 5 GiB, MAX_PARTS = 10_000, MAX_OBJECT_SIZE = 5 TiB, MAX_TAGS = 10, MAX_PRESIGN_EXPIRES = 7 * 24 * 3600.
  • 1.2 Expand S3_CODES via defineCodes("STORAGE_S3", [...] as const) to the ~50 codes listed in design.md#error-code-map. Retain the legacy 11 codes verbatim. Document MULTIPART_FAILED as a back-compat alias.
  • 1.3 Create src/types/sse.types.ts, acl.types.ts, tagging.types.ts, lifecycle.types.ts, cors.types.ts, conditional.types.ts, checksum.types.ts. Wire into src/types/index.ts.
  • 1.4 Extend S3Options in src/types/s3.types.ts with multipart?: MultipartTuning, transport?: FetchLike, signer?: SigV4Signer, defaultSse?: SseConfig, defaultAcl?: CannedAcl.
  • 1.5 Create src/utils/error-mapper.ts — maps S3ServiceException ($metadata.httpStatusCode, name) to the appropriate STORAGE_S3.* code (412 → PRECONDITION_FAILED, 304 → NOT_MODIFIED, 416 → RANGE_NOT_SATISFIABLE, etc.). Sibling .md + .test.ts.

2. AbstractStorage surface — production-grade

  • 2.1 src/core/get/ — native Range GET via Range: header; conditional headers (IfMatch, IfNoneMatch, IfModifiedSince, IfUnmodifiedSince); SSE-C key passthrough; versionId passthrough; checksum verification. Sibling .md + .test.ts.
  • 2.2 src/core/set/ — auto-promote-to-multipart branch over multipart.threshold; single PUT below; SSE / ACL / tagging / metadata / checksum applied at the PUT boundary. Sibling .md + .test.ts.
  • 2.3 src/core/set-stream/ — setStream(key, stream, opts?) that consumes ReadableStream<Uint8Array> and routes through the multipart engine without buffering the whole stream. Sibling .md + .test.ts.
  • 2.4 src/core/set-partial/ — branches: offset === 0 && bytes.byteLength > threshold → multipart engine; otherwise defaultSetPartial. capabilities.partialWrites documented as false. Sibling .md + .test.ts.
  • 2.5 src/core/delete/ — versioned-delete via versionId; conditional delete via IfMatch. Sibling .md + .test.ts.
  • 2.6 src/core/head/ — full metadata bag (eTag, versionId, lastModified, storageClass, content-*, user-metadata, checksum, SSE algorithm). Sibling .md + .test.ts.
  • 2.7 src/core/list/ — listObjectsV2 paginator with prefix, delimiter, maxKeys, continuationToken, startAfter. Sibling .md + .test.ts.
  • 2.8 src/core/exists/ — branch on 404 / 200; documented to use HEAD. Sibling .md + .test.ts.

3. Multipart engine (whole-object writes + UploadPartCopy)

  • 3.1 src/multipart/part-chunker.ts + .md + .test.ts — covers exact multiples, ragged final part, empty stream, mid-read abort.
  • 3.2 src/multipart/worker-pool.ts + .md + .test.ts — bounded-concurrency scheduler with linked AbortController; covers cap enforcement, first-failure abort-of-peers, caller-abort propagation.
  • 3.3 src/multipart/multipart-engine.ts + .md + .test.ts — owns init / parts / complete / abort lifecycle; maps each stage failure to its dedicated code.
  • 3.4 src/multipart/upload-part-copy.ts + .md + .test.ts — server-side multipart copy for objects > 5 GiB. Splits source by CopySourceRange.
  • 3.5 src/multipart/index.ts — explicit named re-exports only (§6a).

4. S3-specific extensions

  • 4.1 src/presign/get-signed-get-url.ts + sibling docs + tests — TTL + response-content-* + version + SSE forwarding.
  • 4.2 src/presign/get-signed-put-url.ts + docs + tests — TTL + content pin + SSE + checksum forwarding.
  • 4.3 src/presign/get-signed-delete-url.ts + docs + tests — TTL + version-id support.
  • 4.4 src/sse/ — SSE-S3 / SSE-KMS / SSE-C parameter shaping; keyMd5 validation against MD5(key) at the boundary.
  • 4.5 src/acl/ — canned ACL + grant-policy parameter shaping; getObjectAcl / putObjectAcl.
  • 4.6 src/copy/ — copyObject within-bucket + cross-bucket (via sourceBucket); metadata directive (COPY / REPLACE); tagging directive; conditional headers; SSE override; auto-route to upload-part-copy when source > 5 GiB.
  • 4.7 src/tagging/ — getObjectTags / putObjectTags / deleteObjectTags; inline tags at set time URL-encoded into x-amz-tagging.
  • 4.8 src/versioning/ — listObjectVersions paginator; versionId passthrough on get / head / delete.
  • 4.9 src/lifecycle/ — get / put / delete bucket lifecycle rules.
  • 4.10 src/cors/ — get / put / delete bucket CORS rules.
  • 4.11 src/bucket/ — createBucket / deleteBucket / bucketExists / getBucketRegion.
  • 4.12 src/conditional/ — shared module that translates ConditionalOptions into the right AWS-SDK input fields, used by get / head / delete / copy.
  • 4.13 src/checksum/ — SHA-256 + CRC32C computation + verification on single PUT and on get-with-checksum.

5. Transport / signer / retry plug-points

  • 5.1 src/transport/transport.ts + docs + tests — FetchLike adapter threaded through S3ClientFactory.create via requestHandler / Fetch.
  • 5.2 src/signer/signer.ts + docs + tests — pluggable SigV4Signer; defaults to AWS SDK's SignatureV4.
  • 5.3 src/retry/retry.ts + docs + tests — replace withRetry in s3.ts with createRetryStrategy from @suaveplan/retry. Preserve isRetryableS3Error classification (5xx / throttling / SlowDown / RequestTimeout). Delete the old withRetry helper.

6. Top-level S3 class wiring

  • 6.1 Refactor src/s3/s3.ts so it owns construction (validateCredentials, S3Client build with transport/signer plug-points), capability flags (partialReads: true, nativeStreams: true, partialWrites: false), and delegates each surface to its co-located module. Keep S3 class ≤ 300 lines.
  • 6.2 Validate multipart.partSize ∈ [MIN_PART_SIZE, MAX_PART_SIZE], multipart.threshold ≥ MIN_PART_SIZE, multipart.concurrency ∈ [1, 32] at construction time; throw S3StorageError(MULTIPART_INIT_FAILED, ...).
  • 6.3 Update src/index.ts root barrel to namespace-re-export every new module (export * as multipart from "./multipart/index.js", export * as presign from "./presign/index.js", etc.) per §6a.

7. Subpath exports + package.json

  • 7.1 Add subpath exports in package.json for every feature module under src/: ./core/get/get, ./core/set/set, ./core/set-stream/set-stream, ./multipart/multipart-engine, ./multipart/part-chunker, ./multipart/worker-pool, ./multipart/upload-part-copy, ./presign/get-signed-get-url, ./presign/get-signed-put-url, ./presign/get-signed-delete-url, ./sse/sse, ./acl/acl, ./copy/copy, ./tagging/tagging, ./versioning/versioning, ./lifecycle/lifecycle, ./cors/cors, ./bucket/bucket, ./conditional/conditional, ./checksum/checksum, ./transport/transport, ./signer/signer, ./retry/retry, ./utils/error-mapper.
  • 7.2 Add @suaveplan/retry to dependencies if not already present (it is — verify workspace:^).

8. Integration tests against Garage (real service, no mocks)

  • 8.1 tests/integration/core.test.ts — get / set / delete / list / head round-trips against the Garage fixture.
  • 8.2 tests/integration/multipart.test.ts — 10 cases per design table (auto-promote, concurrency cap, abort+cleanup, per-stage error codes, setPartial whole-object, part-size validation).
  • 8.3 tests/integration/stream.test.ts — setStream of 25 MiB → 5 parts at 5 MiB; round-trip bytes verify.
  • 8.4 tests/integration/presign.test.ts — all three presigned URLs; TTL boundary; header pin; response-content-disposition override.
  • 8.5 tests/integration/sse.test.ts — SSE-S3, SSE-KMS (Garage compatibility), SSE-C with key+MD5 validation; SSE-C mismatch raises SSE_KEY_MISMATCH before request leaves process.
  • 8.6 tests/integration/acl.test.ts — canned + grants + getObjectAcl + putObjectAcl.
  • 8.7 tests/integration/copy.test.ts — within-bucket, cross-bucket (second bucket created via fixture extension), > 5 GiB routed via UploadPartCopy (simulated via fixture-level threshold override since a real > 5 GiB object is impractical in CI).
  • 8.8 tests/integration/tagging.test.ts — inline-at-set, post-write put, get, delete; > 10 tags raises TAG_LIMIT_EXCEEDED.
  • 8.9 tests/integration/versioning.test.ts — list versions, get by versionId, delete by versionId.
  • 8.10 tests/integration/lifecycle.test.ts — round-trip lifecycle rules; delete; get-after-delete raises 404.
  • 8.11 tests/integration/cors.test.ts — round-trip CORS rules; delete; get-after-delete raises 404.
  • 8.12 tests/integration/bucket.test.ts — create / exists / region / delete; create-existing raises BUCKET_ALREADY_EXISTS; delete-non- empty raises BUCKET_NOT_EMPTY.
  • 8.13 tests/integration/conditional.test.ts — IfMatch hit → 200; IfMatch miss → PRECONDITION_FAILED; IfNoneMatch / If-*Modified- Since paths.
  • 8.14 tests/integration/range.test.ts — partial GET via native Range; OOB raises RANGE_NOT_SATISFIABLE.
  • 8.15 tests/integration/checksum.test.ts — SHA-256 + CRC32C round trip; deliberately corrupted bytes raise CHECKSUM_MISMATCH.
  • 8.16 Extend @suaveplan/testing-integration-garage-s3 fixture surface only as needed (e.g. expose a second-bucket helper for cross-bucket copy; expose ListPartsCommand for mid-flight assertions). Any extension lands in this change.

9. Docs

  • 9.1 Rewrite README.md Modules + Features sections to cover the full surface (one bullet per module under §3 in this tasks file).
  • 9.2 Co-located .md for every implementation file per project rule §4 (≥ 200 words, basic + advanced example, API reference, implementation notes).
  • 9.3 Update error-codes.md to enumerate every code with one-sentence semantics + the call site that raises it.

10. Verify (per §13 pre-ship sequence)

  • 10.1 Spec reconciliation (§13 step 0): walk every ### Requirement and #### Scenario in specs/storage-s3/spec.md; confirm matching impl + verbatim **Verified by:** test names.
  • 10.2 bunx turbo lint typecheck test --filter=@suaveplan/storage-s3 green.
  • 10.3 bunx turbo test:coverage --filter=@suaveplan/storage-s3 100/100/100/100.
  • 10.4 bunx turbo test:integration --filter=@suaveplan/storage-s3 green against the Garage testcontainer.
  • 10.5 Dependent sweep: bunx turbo test --filter=...@suaveplan/storage-s3 green.
  • 10.6 Banned-pattern / barrel-shape / runner-import scans (§13 steps 4 / 4a / 4b) — zero output.
  • 10.7 bun scripts/openspec-conformance.ts @suaveplan/storage-s3 PASS.
  • 10.8 openspec validate complete-storage-s3 --strict PASS.

11. Archive

  • 11.1 Refresh root docs per §13b — MANIFEST.md, MISSING.yml, STATUS.md, SUMMARY.md; run bunx markdownlint-cli2 --fix.
  • 11.2 Fold specs/storage-s3/spec.md into openspec/specs/storage-s3/spec.md (this is the first change to establish that canonical capability file — create the folder under openspec/specs/ only at this archival step).
  • 11.3 Move openspec/changes/complete-storage-s3/ → openspec/archive/<yyyy-mm-dd>-complete-storage-s3/.