# S3

Lightweight, dependency-free fake of AWS S3 that speaks the real S3 REST API (XML wire protocol), so application code using `@aws-sdk/client-s3` can run against it with zero cost and zero side effects.

| Key | Value |
|-----|-------|
| Port | 4566 |
| Protocol | AWS S3 REST API (HTTP + XML) |
| Compatible client | `@aws-sdk/client-s3` (v3) |
| Size | ~96 KB |
| Startup | < 100ms |
| State | In-memory, ephemeral, resettable |

## Quick Start

Start the server:

```js
import { S3Server } from "./services/s3/src/server.js";

const server = new S3Server(4566);
await server.start();
// ... use it ...
await server.stop();
```

Connect with the real AWS SDK client:

```js
import {
  S3Client,
  CreateBucketCommand,
  PutObjectCommand,
  GetObjectCommand,
} from "@aws-sdk/client-s3";

const s3 = new S3Client({
  region: "us-east-1",
  endpoint: "http://127.0.0.1:4566",
  forcePathStyle: true, // recommended for the fake
  credentials: { accessKeyId: "parlel", secretAccessKey: "parlel" },
});

await s3.send(new CreateBucketCommand({ Bucket: "my-bucket" }));
await s3.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "hello.txt", Body: "hello world" }));

const obj = await s3.send(new GetObjectCommand({ Bucket: "my-bucket", Key: "hello.txt" }));
console.log(await obj.Body.transformToString()); // "hello world"
```

### Addressing

Both addressing styles are supported:

- **Path-style** (recommended): `http://127.0.0.1:4566/{bucket}/{key}` — set `forcePathStyle: true`.
- **Virtual-hosted-style**: `http://{bucket}.s3.amazonaws.com/{key}` — resolved from the `Host` header.

### Authentication

SigV4 signatures are **accepted but not verified** (any credentials work). This matches LocalStack-style local development.

## Access via Parlel Sandbox

S3 is an HTTP service, so inside a Parlel sandbox it is exposed directly at its
own Daytona preview URL (not through the MCP `parlel_execute` tool, which is for
TCP database services). Point any `@aws-sdk/client-s3` client — or plain
HTTP — at the preview URL from the sandbox's **Connect** panel:

```bash
# the preview token comes from the Connect panel / GET /connection
curl -X PUT "$S3_URL/my-bucket" -H "x-daytona-preview-token: $TOKEN"
curl -X PUT "$S3_URL/my-bucket/hello.txt" -H "x-daytona-preview-token: $TOKEN" --data "hello world"
curl "$S3_URL/my-bucket/hello.txt" -H "x-daytona-preview-token: $TOKEN"   # -> hello world
```

## Internal / Health Endpoints

These are not part of the S3 API; they support local tooling.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/_parlel/health` | Returns `{ "status": "ok", "service": "s3", "buckets": N }` |
| `POST` | `/_parlel/reset` | Clears all in-memory state |

You can also reset programmatically via `server.reset()`.

## Implemented Operations

### Service

| Operation | SDK Command | Notes |
|-----------|-------------|-------|
| ListBuckets | `ListBucketsCommand` | `GET /` |

### Bucket — core

| Operation | SDK Command | Notes |
|-----------|-------------|-------|
| CreateBucket | `CreateBucketCommand` | Validates bucket name; `BucketAlreadyOwnedByYou` on re-create |
| DeleteBucket | `DeleteBucketCommand` | `BucketNotEmpty` if it has live objects |
| HeadBucket | `HeadBucketCommand` | 200 / 404 + `x-amz-bucket-region` |
| GetBucketLocation | `GetBucketLocationCommand` | Empty constraint for `us-east-1` |

### Bucket — configuration

| Operation | SDK Command |
|-----------|-------------|
| GetBucketVersioning / PutBucketVersioning | `GetBucketVersioningCommand` / `PutBucketVersioningCommand` |
| GetBucketTagging / PutBucketTagging / DeleteBucketTagging | `GetBucketTaggingCommand` / `PutBucketTaggingCommand` / `DeleteBucketTaggingCommand` |
| GetBucketCors / PutBucketCors / DeleteBucketCors | `GetBucketCorsCommand` / `PutBucketCorsCommand` / `DeleteBucketCorsCommand` |
| GetBucketPolicy / PutBucketPolicy / DeleteBucketPolicy | `GetBucketPolicyCommand` / `PutBucketPolicyCommand` / `DeleteBucketPolicyCommand` |
| GetBucketAcl / PutBucketAcl | `GetBucketAclCommand` / `PutBucketAclCommand` |
| GetBucketLifecycleConfiguration / PutBucketLifecycleConfiguration / DeleteBucketLifecycle | `GetBucketLifecycleConfigurationCommand` / `PutBucketLifecycleConfigurationCommand` / `DeleteBucketLifecycleCommand` |
| GetBucketEncryption / PutBucketEncryption / DeleteBucketEncryption | `GetBucketEncryptionCommand` / `PutBucketEncryptionCommand` / `DeleteBucketEncryptionCommand` |
| GetBucketWebsite / PutBucketWebsite / DeleteBucketWebsite | `GetBucketWebsiteCommand` / `PutBucketWebsiteCommand` / `DeleteBucketWebsiteCommand` |

> Configuration documents (CORS, lifecycle, encryption, website) are stored and returned verbatim so the SDK round-trips them faithfully.

### Object — core

| Operation | SDK Command | Notes |
|-----------|-------------|-------|
| PutObject | `PutObjectCommand` | MD5 ETag, user metadata, `x-amz-tagging`, Content-MD5 validation (`BadDigest`) |
| GetObject | `GetObjectCommand` | Range requests (206 / `InvalidRange`), conditional headers, `response-*` overrides |
| HeadObject | `HeadObjectCommand` | Metadata only; 404 on miss |
| DeleteObject | `DeleteObjectCommand` | Idempotent; inserts a delete marker when versioning is enabled |
| DeleteObjects | `DeleteObjectsCommand` | Batch delete, supports `Quiet` |
| CopyObject | `CopyObjectCommand` | `x-amz-copy-source`, `COPY`/`REPLACE` metadata & tagging directives |

### Object — listing

| Operation | SDK Command | Notes |
|-----------|-------------|-------|
| ListObjects (v1) | `ListObjectsCommand` | `prefix`, `delimiter`, `marker`, `max-keys`, `CommonPrefixes` |
| ListObjectsV2 | `ListObjectsV2Command` | `continuation-token`, `start-after`, `fetch-owner`, `KeyCount` |
| ListObjectVersions | `ListObjectVersionsCommand` | Versions + delete markers, `IsLatest` |

### Object — tagging / ACL / attributes

| Operation | SDK Command |
|-----------|-------------|
| GetObjectTagging / PutObjectTagging / DeleteObjectTagging | `GetObjectTaggingCommand` / `PutObjectTaggingCommand` / `DeleteObjectTaggingCommand` |
| GetObjectAcl / PutObjectAcl | `GetObjectAclCommand` / `PutObjectAclCommand` |
| GetObjectAttributes | `GetObjectAttributesCommand` |

### Multipart uploads

| Operation | SDK Command | Notes |
|-----------|-------------|-------|
| CreateMultipartUpload | `CreateMultipartUploadCommand` | Returns `UploadId` |
| UploadPart | `UploadPartCommand` | Per-part MD5 ETag; part number 1–10000 |
| CompleteMultipartUpload | `CompleteMultipartUploadCommand` | Multipart ETag `md5(part-md5s)-N`; validates order & parts |
| AbortMultipartUpload | `AbortMultipartUploadCommand` | Discards the upload |
| ListParts | `ListPartsCommand` | Lists uploaded parts |
| ListMultipartUploads | `ListMultipartUploadsCommand` | Lists in-progress uploads |

## Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

| Feature | Status |
|---------|--------|
| Buckets: create / delete / list / head / location | ✅ Supported |
| Objects: put / get / head / delete / copy | ✅ Supported |
| Batch delete | ✅ Supported |
| Listing v1 + v2 with prefix/delimiter/pagination | ✅ Supported |
| Range + conditional GET (`If-Match`, `If-None-Match`, `If-(Un)Modified-Since`) | ✅ Supported |
| User metadata (`x-amz-meta-*`) | ✅ Supported |
| Object & bucket tagging | ✅ Supported |
| Object & bucket ACL (canned, static owner) | ✅ Supported |
| Versioning + delete markers + version listing | ✅ Supported |
| Multipart uploads (full lifecycle) | ✅ Supported |
| Bucket config: CORS / policy / lifecycle / encryption / website | ✅ Stored & round-tripped |
| GetObjectAttributes | ✅ Supported |
| Virtual-hosted & path-style addressing | ✅ Supported |
| SigV4 signature **verification** | ⚠️ Accepted but not enforced |
| Server-side encryption (actual crypto) | ⚠️ Config stored, data is not encrypted |
| POST form (browser) uploads | ⟳ Roadmap — `NotImplemented` (501) |
| Replication, logging, notification, inventory, analytics, metrics, object-lock, request-payment, accelerate, public-access-block, ownership-controls, intelligent-tiering, S3 Select | ⟳ Roadmap — `NotImplemented` (501) |
| Presigned URL signature validation | ⚠️ URL routing works; signature not checked |

## Error Codes & Shapes

Errors use the standard S3 XML envelope:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>NoSuchKey</Code>
  <Message>The specified key does not exist.</Message>
  <Key>missing.txt</Key>
  <Resource>/my-bucket/missing.txt</Resource>
  <RequestId>…</RequestId>
</Error>
```

| Code | HTTP | When |
|------|------|------|
| `NoSuchBucket` | 404 | Bucket does not exist |
| `NoSuchKey` | 404 | Object/version does not exist |
| `NoSuchUpload` | 404 | Multipart upload id not found |
| `NoSuchTagSet` | 404 | Bucket tagging requested but unset |
| `NoSuchCORSConfiguration` | 404 | Bucket CORS requested but unset |
| `NoSuchBucketPolicy` | 404 | Bucket policy requested but unset |
| `NoSuchLifecycleConfiguration` | 404 | Lifecycle requested but unset |
| `NoSuchWebsiteConfiguration` | 404 | Website config requested but unset |
| `ServerSideEncryptionConfigurationNotFoundError` | 404 | Encryption requested but unset |
| `InvalidBucketName` | 400 | Bucket name fails S3 naming rules |
| `BucketAlreadyOwnedByYou` | 409 | Re-creating an existing bucket |
| `BucketNotEmpty` | 409 | Deleting a bucket with live objects |
| `BadDigest` | 400 | `Content-MD5` does not match the body |
| `InvalidRange` | 416 | Unsatisfiable `Range` header |
| `InvalidPart` | 400 | Completing multipart with a missing/mismatched part |
| `InvalidPartOrder` | 400 | Parts not in ascending order |
| `PreconditionFailed` | 412 | `If-Match` / `If-Unmodified-Since` failed |
| `NotModified` | 304 | `If-None-Match` / `If-Modified-Since` matched |
| `NotImplemented` | 501 | Operation/sub-resource not implemented |
| `MethodNotAllowed` | 405 | Method not valid for the resource |
| `InternalError` | 500 | Unexpected server error |

## Environment Variables

The manifest advertises these defaults for clients/tooling:

| Variable | Default |
|----------|---------|
| `AWS_ACCESS_KEY_ID` | `parlel` |
| `AWS_SECRET_ACCESS_KEY` | `parlel` |
| `AWS_REGION` | `us-east-1` |
| `AWS_ENDPOINT_URL_S3` | `http://127.0.0.1:4566` |
| `AWS_S3_FORCE_PATH_STYLE` | `true` |
