## assay.s3

S3-compatible object storage with AWS Signature V4 authentication.
Client: `s3.client({endpoint="...", region="...", access_key="...", secret_key="...", path_style=true})`.
Works with AWS S3, Cloudflare R2, iDrive e2, MinIO, and any S3-compatible provider.

### Buckets

- `c:create_bucket(bucket)` → true — Create a new bucket
- `c:delete_bucket(bucket)` → true — Delete a bucket
- `c:list_buckets()` → `[{name, creation_date}]` — List all buckets
- `c:bucket_exists(bucket)` → bool — Check if bucket exists

### Objects

- `c:put_object(bucket, key, body, opts?)` → true — Upload object. `opts`: `{content_type}`
- `c:get_object(bucket, key)` → string|nil — Download object content (nil if 404)
- `c:delete_object(bucket, key)` → true — Delete an object
- `c:list_objects(bucket, opts?)` → `{objects, is_truncated, next_continuation_token, key_count}` — List objects. `opts`: `{prefix, max_keys, continuation_token}`. Each object: `{key, size, last_modified}`
- `c:head_object(bucket, key)` → `{status, headers}`|nil — Get object metadata (nil if 404)
- `c:copy_object(src_bucket, src_key, dst_bucket, dst_key)` → true — Copy object between buckets

Example:
```lua
local s3 = require("assay.s3")
local c = s3.client({
  endpoint = "https://s3.us-east-1.amazonaws.com",
  region = "us-east-1",
  access_key = env.get("AWS_ACCESS_KEY_ID"),
  secret_key = env.get("AWS_SECRET_ACCESS_KEY"),
})
c:put_object("my-bucket", "data/report.json", json.encode({status = "complete"}))
local content = c:get_object("my-bucket", "data/report.json")
```
