# Use Cloudflare R2

```yaml
capability_id: r2.use
display_name: Use Cloudflare R2
scope: r2:use
endpoint:
  method: POST
  path: /r2/presign
operation: r2.presign
billable: true
commands:
  - GetObject
  - PutObject
environment_selector: none
```

## Purpose and authorization

The `r2:use` scope authorizes an authenticated application to request a
presigned Cloudflare R2 `GetObject` or `PutObject` operation. The application
must use [Contract v1](https://docs.getgateman.com/contract-v1/). The
documentation identifier `r2.use` and runtime scope `r2:use` refer to the same
capability at different layers.

Gateman uses the application's configured Cloudflare R2 credentials to sign a
native S3-compatible request and returns the URL, method, and headers needed
for the application to call R2 directly. Gateman does not return the secret
access key, transfer object content, execute the subsequent GET or PUT, check
bucket or object existence, register buckets, enforce key prefixes, or add
object-level authorization. Cloudflare credential permissions are the
provider-side resource and action authorization boundary.

## Implementation contract

The outer request is authenticated with Contract v1:

```http
POST /r2/presign
Content-Type: application/json
X-GM-App: app_...
X-GM-Ts: 1710000000
X-GM-Sig: <Contract-v1-signature>
X-GM-Idempotency-Key: optional-gateman-key
```

`X-GM-Idempotency-Key` is optional and participates in Contract v1 signing.
It does not create an R2-specific replay ledger or deduplicate presigning
operations.

The JSON body has this exact top-level contract:

| Field | Required | Contract |
| --- | --- | --- |
| `operation` | Yes | `get` or `put`; selects native `GetObject` or `PutObject`. |
| `bucket` | Yes | Lowercase R2 bucket name, 3–63 bytes, beginning and ending with a letter or digit and containing only lowercase letters, digits, and hyphens. |
| `key` | Yes | Nonempty UTF-8 object key, at most 1,024 bytes, without control characters. The key remains opaque to Gateman. |
| `jurisdiction` | Yes | One of `default`, `eu`, `us`, `fedramp`, or `fedramp-high`. |
| `expires` | Yes | Integer seconds from 1 through 3,600. |
| `parameters` | No | JSON object containing native SDK inputs for the selected operation. |
| `content_type` | No | Legacy PUT compatibility field equivalent to native `ContentType`. |

Unknown top-level fields are rejected. The caller cannot provide an endpoint,
host, region, service, credential, or replacement header configuration.

## Native parameters

`parameters` is validated as an object against the installed AWS SDK operation
model. It is not an unchecked JSON bag or a separate exhaustive Gateman
allowlist. Native names and value types are checked for the selected
`GetObject` or `PutObject` operation before operation admission.

- The outer `bucket` and `key` become the trusted SDK `Bucket` and `Key`
  inputs.
- Nested `Bucket`, `Key`, and `Body` values are rejected.
- Unknown native parameter names and SDK-invalid value types are rejected.
- `ChecksumAlgorithm` is rejected because Gateman never receives an object
  body from which to calculate the requested checksum.
- Caller-supplied explicit checksum values may be accepted and signed when
  the installed SDK can serialize them. This does not claim that Cloudflare
  accepts every AWS checksum algorithm.
- Native fields can become signed query parameters, request headers, or both.
  The caller must use the returned request exactly.

For PUT, the preferred form is native `parameters.ContentType`. The legacy
top-level `content_type` remains supported. Matching values may be supplied in
both forms; conflicting values return HTTP `400`. `content_type` is rejected
for GET. Native GET response-content parameters remain available through
`parameters`.

## GET and PUT examples

### Native GET

```json
{
  "operation": "get",
  "bucket": "example-bucket",
  "key": "documents/report.pdf",
  "jurisdiction": "eu",
  "expires": 300,
  "parameters": {
    "Range": "bytes=0-1048575",
    "IfMatch": "\"example-etag\""
  }
}
```

Depending on the native fields, the completed request can contain signed
query parameters and required request headers for range retrieval, conditional
retrieval, or response overrides. Send the returned method, URL, and every
returned header to R2. The application receives and interprets the actual R2
object response directly.

### Native PUT

```json
{
  "operation": "put",
  "bucket": "example-bucket",
  "key": "uploads/photo.jpg",
  "jurisdiction": "eu",
  "expires": 900,
  "parameters": {
    "ContentType": "image/jpeg",
    "Metadata": {
      "source": "website"
    },
    "CacheControl": "max-age=3600",
    "StorageClass": "STANDARD"
  }
}
```

The application sends the object bytes directly to R2 using the returned PUT
URL and headers. Gateman neither receives the bytes nor verifies that R2
stored them.

## Response contract and signed headers

A successful response is HTTP `200`:

```json
{
  "ok": true,
  "url": "https://example-bucket.<account>.eu.r2.cloudflarestorage.com/uploads/photo.jpg?...",
  "method": "PUT",
  "expires_at": "2026-10-16T12:15:00+00:00",
  "headers": {
    "Content-Type": "image/jpeg",
    "x-amz-meta-source": "website"
  }
}
```

`url` is the complete presigned URL, `method` is `GET` or `PUT`, and
`expires_at` is the server-calculated UTC expiry. `headers` contains the
non-`Host` headers in the completed signed request. Every returned header must
be sent with the returned method and URL. Do not reconstruct or filter the
headers using `X-Amz-SignedHeaders`; some native parameters become query
fields and others become required headers.

The URL is a bearer-like capability until it expires. Protect it from
unnecessary disclosure, and do not alter its path encoding or query string.
Gateman does not revoke individual URLs or retain an R2 issuance ledger.

`ContentLength` can be included as a native PUT parameter. The current signer
returns a signed `Content-Length` header, so the value is covered by the local
SigV4 signature. This is not an independent Gateman upload-size policy and
does not prove Cloudflare's runtime enforcement.

## Cloudflare endpoint, jurisdiction, and credentials

Gateman derives the endpoint from the application's configured Cloudflare
account ID and the requested jurisdiction:

- `default`: `https://<account-id>.r2.cloudflarestorage.com`
- other accepted values: `https://<account-id>.<jurisdiction>.r2.cloudflarestorage.com`

The accepted Gateman jurisdiction values are `default`, `eu`, `us`, `fedramp`,
and `fedramp-high`. The SigV4 service is `s3` and the signing region is always
`auto`; this is not AWS regional selection. The caller cannot replace the
account ID, endpoint hostname, credentials, service, or signing region.

Gateman stores and retrieves these application-scoped encrypted secrets:

- `CLOUDFLARE_R2_ACCESS_KEY_ID`
- `CLOUDFLARE_R2_SECRET_ACCESS_KEY`

Using `/r2/presign` does not require `secrets:read` and does not require the
Cloudflare R2 **Allow app retrieval** setting. Direct credential retrieval is a
separate capability and authorization path. Cloudflare's R2 token or access
key permissions determine whether the eventual bucket, object, and action are
allowed; a successful presign response does not guarantee that R2 will accept
the later request.

The capability is environment-independent. It has no `environment` field,
does not select staging or development credentials, and does not use Official
client environment injection.

`fedramp-high` is accepted by the Gateman contract and is used to derive the
jurisdiction-specific endpoint. Provider-specific endpoint availability and
acceptance remain Cloudflare concerns; this reference does not assert an
additional provider guarantee.

## Cloudflare provider compatibility

Three boundaries must remain distinct:

1. The installed AWS SDK model recognizes a native parameter name and value.
2. Gateman can serialize and sign that native request locally.
3. Cloudflare R2 accepts and executes the corresponding feature.

Cloudflare documents an S3-compatible API, but it does not implement every
AWS S3 feature. The following are useful implementation distinctions:

| Feature | Documentation treatment |
| --- | --- |
| PUT content headers such as `Content-Type`, cache control, disposition, encoding, language, and explicit content MD5 | Supported by Cloudflare where documented. |
| User-defined `Metadata` | Supported; SDK metadata becomes `x-amz-meta-*` headers. |
| Range and conditional GET headers | Supported by Cloudflare's S3 API. |
| `STANDARD` and `STANDARD_IA` storage classes | Supported Cloudflare R2 storage classes. |
| PUT `Tagging` | Cloudflare documents object tagging as unsupported; do not use it as a primary R2 success example. Gateman does not add a provider-specific static rejection. |
| AWS ACLs and object-lock features | Cloudflare documents these AWS features as unsupported. Gateman does not add a provider-specific static rejection. |
| AWS SSE/KMS options | Cloudflare compatibility differs from AWS; do not infer support from SDK recognition. |
| SSE-C | Cloudflare documents support. |
| Response overrides and object-version parameters | Do not claim runtime support without current Cloudflare evidence. |

See [Cloudflare S3 API compatibility](https://developers.cloudflare.com/r2/api/s3/api/),
[presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/),
and [S3 API extensions](https://developers.cloudflare.com/r2/api/s3/extensions/).
A successful Gateman response means that the request was locally signed, not
that Cloudflare accepted or executed every SDK-valid feature.

## Body, checksums, and operation accounting

`parameters.Body` is rejected. Object content must bypass Gateman and go
directly from the application to R2. Gateman does not buffer, hash, forward,
or inspect the object body.

`parameters.ChecksumAlgorithm` is rejected because the body is unavailable for
checksum calculation. Explicit caller-supplied checksum values may be signed
where the SDK supports them, but Gateman does not calculate or verify object
checksums.

The billable operation is `r2.presign`. One operation is consumed per admitted
invocation. Validation failures rejected before admission do not consume an
operation. Credential, serialization, signing, or other failures discovered
after admission remain failed billable operations. The later direct R2 GET or
PUT consumes no additional Gateman operation.

Repeated admitted presign requests remain separate operations even when the
optional `X-GM-Idempotency-Key` is reused. That header is Contract v1 input,
not an R2 replay ledger.

Monthly allowance exhaustion returns HTTP `403` with
`monthly_operation_limit_reached`. Application concurrency rejection returns
HTTP `429` with `Retry-After` when applicable. An unavailable application
concurrency guard returns HTTP `503`. Operation-accounting unavailability
returns HTTP `503` with `Operation accounting unavailable`.

## Errors

| Condition | HTTP response | Operation consequence |
| --- | ---: | --- |
| Contract v1 authentication failure or missing `r2:use` | `401` | Rejected before capability execution; no operation. |
| Invalid JSON | `400` | Rejected before admission; no operation. |
| Missing field, unsupported operation, invalid bucket/key/jurisdiction/expiry | `400` | Rejected before admission; no operation. |
| Invalid `parameters` shape | `400` | Rejected before admission; no operation. |
| Unknown top-level or native parameter | `400` | Rejected before admission; no operation. |
| Nested `Bucket`, `Key`, or `Body` | `400` | Rejected before admission; no operation. |
| Rejected `ChecksumAlgorithm` or invalid native value type | `400` | Rejected before admission; no operation. |
| GET `content_type` or conflicting Content-Type values | `400` | Rejected before admission; no operation. |
| Application concurrency rejection | `429`, with `Retry-After` when applicable | No operation. |
| Application concurrency guard unavailable | `503` | No operation. |
| Monthly operation allowance exhausted | `403` | No new operation is reserved. |
| Operation accounting unavailable | `503` | No confirmed operation is reserved. |
| Missing R2 configuration | `400` | Admitted operation is finalized as failed. |
| Missing R2 credentials or credential-store failure | `502` | Admitted operation is finalized as failed. |
| Post-admission SDK signing failure classified as `InvalidArgumentException` | `400` | Admitted operation is finalized as failed. |
| Other unexpected presigning failure | `502` with `R2 presigning unavailable` | Admitted operation is finalized as failed. |

These are Gateman presigning errors. They are distinct from an R2 response
returned later when the application uses the URL, such as an authorization
denial, expired signature, missing object, unsupported provider feature, or
invalid request header. HTTP status alone does not determine whether a
billable operation was consumed.

## Implementation outcome

A correct integration:

- authenticates with Contract v1 and the `r2:use` scope;
- calls `POST /r2/presign` with `get` or `put`, bucket, full opaque key,
  jurisdiction, and expiry;
- supplies native SDK parameters only when needed and respects reserved fields;
- preserves the returned URL, method, and every returned header;
- executes the signed request directly against Cloudflare R2;
- interprets the actual Cloudflare response and provider permissions; and
- accounts for one `r2.presign` operation per admitted Gateman invocation.

## Implementation instructions for agents

- **MUST** authenticate with Contract v1, request the `r2:use` scope, and call
  `POST /r2/presign`.
- **MUST** send `operation` as `get` or `put`, with `bucket`, full `key`,
  supported `jurisdiction`, and integer `expires` from 1 through 3,600.
- **MAY** send native `GetObject` or `PutObject` inputs in the `parameters`
  object. Do not treat it as unchecked JSON.
- **MUST NOT** send nested `Bucket`, `Key`, or `Body`; object bytes never pass
  through Gateman. **MUST NOT** send `ChecksumAlgorithm`; Gateman rejects it
  because object content never passes through Gateman.
- **MAY** use legacy PUT `content_type`, but **MUST NOT** use it for GET or
  conflict it with `parameters.ContentType`.
- **MUST** send every returned header with the returned URL and method. Do not
  filter or reconstruct headers from `X-Amz-SignedHeaders`.
- **MUST** use the configured Cloudflare account context, service `s3`, and
  signing region `auto`; callers cannot override them.
- **MUST** treat Cloudflare credentials and permissions as authoritative for
  buckets, keys, and provider actions.
- **MUST** distinguish local SDK signing success from Cloudflare provider
  compatibility and execution success.
- **MUST** account for each admitted `r2.presign` call and must not invent
  bucket registration, prefix restrictions, object authorization, retries, or
  R2-specific Official client helpers.

## Reusable AGENTS.md block

```markdown
<!-- gateman:begin capability=r2.use -->
- Use Contract v1 with the `r2:use` scope and target `POST /r2/presign`.
- Send `operation` as `get` or `put`, plus the caller-selected `bucket`, full
  opaque `key`, supported `jurisdiction`, and integer `expires` from 1 through
  3,600. R2 has no `environment` selector; signing uses service `s3` and
  region `auto`.
- Put native `GetObject` or `PutObject` inputs in the JSON `parameters` object.
  The installed SDK model validates names and types; it is not an unchecked
  JSON bag or a guarantee that Cloudflare supports every AWS feature.
- Do not send nested `Bucket`, `Key`, or `Body`. Object bytes go directly from
  the application to R2. `ChecksumAlgorithm` is rejected because Gateman does
  not receive the body; explicit caller-supplied checksums may be signed when
  supported by the SDK.
- For PUT, top-level `content_type` is legacy compatibility for
  `parameters.ContentType`; matching values are accepted, conflicts are not.
  Do not use `content_type` for GET.
- Use the returned `url`, `method`, and every header in `headers` unchanged for
  the direct R2 request. Do not filter or reconstruct headers from
  `X-Amz-SignedHeaders`, and interpret the actual Cloudflare response.
- Cloudflare credentials and permissions authorize the bucket, key, and
  provider action. Do not add bucket registration, prefix checks, or object
  authorization to Gateman. Distinguish SDK signing from Cloudflare support.
- One admitted call consumes one `r2.presign` operation. The direct R2
  transfer consumes no additional Gateman operation. `X-GM-Idempotency-Key`
  does not deduplicate R2 presigning calls.
- Full capability reference:
  https://docs.getgateman.com/capabilities/r2.use/
<!-- gateman:end capability=r2.use -->
```

## Related references

- [Contract v1](https://docs.getgateman.com/contract-v1/)
- [Authentication](https://docs.getgateman.com/authentication/)
- [Operation accounting](https://docs.getgateman.com/operation-accounting/)
- [Rate limits and concurrency](https://docs.getgateman.com/rate-limits-and-concurrency/)
- [Idempotency](https://docs.getgateman.com/idempotency/)
- [Errors and responses](https://docs.getgateman.com/errors-and-responses/)
- [Official PHP client](https://docs.getgateman.com/canonical-client-php/)
- [Official Node.js client](https://docs.getgateman.com/canonical-client-node/)
- [Official Python client](https://docs.getgateman.com/canonical-client-python/)
- [Official Go client](https://docs.getgateman.com/canonical-client-go/)
- [Cloudflare R2 S3 API compatibility](https://developers.cloudflare.com/r2/api/s3/api/)
- [Cloudflare R2 presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/)
