Gateman
Documentation/Use Cloudflare R2

Use Cloudflare R2

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. 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:

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:

FieldRequiredContract
operationYesget or put; selects native GetObject or PutObject.
bucketYesLowercase R2 bucket name, 3–63 bytes, beginning and ending with a letter or digit and containing only lowercase letters, digits, and hyphens.
keyYesNonempty UTF-8 object key, at most 1,024 bytes, without control characters. The key remains opaque to Gateman.
jurisdictionYesOne of default, eu, us, fedramp, or fedramp-high.
expiresYesInteger seconds from 1 through 3,600.
parametersNoJSON object containing native SDK inputs for the selected operation.
content_typeNoLegacy 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

{
  "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

{
  "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:

{
  "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:

FeatureDocumentation treatment
PUT content headers such as Content-Type, cache control, disposition, encoding, language, and explicit content MD5Supported by Cloudflare where documented.
User-defined MetadataSupported; SDK metadata becomes x-amz-meta-* headers.
Range and conditional GET headersSupported by Cloudflare's S3 API.
STANDARD and STANDARD_IA storage classesSupported Cloudflare R2 storage classes.
PUT TaggingCloudflare 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 featuresCloudflare documents these AWS features as unsupported. Gateman does not add a provider-specific static rejection.
AWS SSE/KMS optionsCloudflare compatibility differs from AWS; do not infer support from SDK recognition.
SSE-CCloudflare documents support.
Response overrides and object-version parametersDo not claim runtime support without current Cloudflare evidence.

See Cloudflare S3 API compatibility, presigned URLs, and S3 API 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

ConditionHTTP responseOperation consequence
Contract v1 authentication failure or missing r2:use401Rejected before capability execution; no operation.
Invalid JSON400Rejected before admission; no operation.
Missing field, unsupported operation, invalid bucket/key/jurisdiction/expiry400Rejected before admission; no operation.
Invalid parameters shape400Rejected before admission; no operation.
Unknown top-level or native parameter400Rejected before admission; no operation.
Nested Bucket, Key, or Body400Rejected before admission; no operation.
Rejected ChecksumAlgorithm or invalid native value type400Rejected before admission; no operation.
GET content_type or conflicting Content-Type values400Rejected before admission; no operation.
Application concurrency rejection429, with Retry-After when applicableNo operation.
Application concurrency guard unavailable503No operation.
Monthly operation allowance exhausted403No new operation is reserved.
Operation accounting unavailable503No confirmed operation is reserved.
Missing R2 configuration400Admitted operation is finalized as failed.
Missing R2 credentials or credential-store failure502Admitted operation is finalized as failed.
Post-admission SDK signing failure classified as InvalidArgumentException400Admitted operation is finalized as failed.
Other unexpected presigning failure502 with R2 presigning unavailableAdmitted 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

<!-- 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 -->