JacobLinCool / JacobLinCool/particeps

Ship an R2-only Cloudflare ciphertext receiver

Open
#12 0 comments 0 reactions 0 assignees View on GitHub
area: receiver P1
Dominant language
Kotlin
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

## Summary

Ship a deliberately narrow TypeScript Cloudflare Worker that accepts automatic Protocol v1 uploads and durably stores their ciphertext in R2.

The Worker is an untrusted, bounded ingress—not an enrollment, device-authentication, analysis, or management service. Its only successful mutation is creating an immutable R2 object and returning the matching receipt.

## Requirements

### Scope and trust boundary

- Expose one Protocol v1 upload `POST` route.
- Do not expose list, download, delete, admin, dashboard, configuration, participant, or management APIs.
- Do not use D1, Queues, KV, Durable Objects, a private decryption key, or any decrypt/parse path.
- Treat ingress as public and unauthenticated. A deploy-time allowlist may contain accepted configuration digests and researcher signer fingerprints, but these cleartext claims are not participant or device authentication.
- Fix the accepted protocol version, media type, header set, configuration allowlist, and R2 binding at deployment. Do not add remote configuration.
- Give researchers read access directly through least-privilege, S3-compatible R2 credentials outside the public Worker.

### Request validation and streaming

- Reject every method, path, media type, transfer shape, or required header not defined by Protocol v1.
- Require and strictly parse `bundle_id`, `Content-Length`, SHA-256 `Content-Digest`, exact claimed sequence range, event count, configuration digest, and producer metadata defined by #8/#9.
- Enforce a 32 MiB body hard limit before and while reading. Reject missing, conflicting, malformed, truncated, overlong, or trailing input.
- Stream the request `ReadableStream` directly into R2. Do not buffer the complete body in Worker memory.
- Supply the expected SHA-256 to R2 so the durable object write itself validates content integrity.
- Use `bundle_id` as the immutable R2 object key under a fixed deployment namespace.
- Store bounded R2 custom metadata for ciphertext SHA-256, byte count, claimed range, claimed event count, configuration digest, producer platform/version, and receive time. Mark all routing metadata as untrusted claims.

### Idempotency and receipts

- Use a create-only conditional R2 write so an existing `bundle_id` can never be overwritten.
- After a successful durable new write, return `201 Created` with a canonical receipt containing the exact stored identity, digest, size, range, event count, and configuration digest.
- If the same ID already exists and its object checksum and metadata are identical, return `200 OK` with the identical receipt.
- If the same ID exists with any different digest, size, or metadata, return `409 Conflict`; never replace either object.
- Handle concurrent identical or conflicting requests safely. Rely on R2 conditional writes and strong consistency, not a non-atomic read-then-overwrite sequence.
- Return no 2xx until R2 confirms durable persistence. A lost response followed by an exact replay must be safe.

### Abuse and operations

- Provide a Wrangler deployment template with explicit bindings, secrets/configuration, routes, compatibility date, and environment separation.
- Configure or document Cloudflare WAF/rate limiting, a fixed request bound, deployment isolation, R2 lifecycle/retention policy, storage-budget alerts, and emergency ingress disablement.
- Log only bounded operational reason codes, response status, coarse timing, and non-sensitive aggregate sizes. Do not log participant UUIDs, configuration artifacts, raw headers, URLs containing secrets, or body bytes.
- Expose no claim of participant authenticity, device authenticity, enrollment, completeness, or decrypted validity in receipts or documentation.

## Acceptance criteria

- A valid upload is streamed to R2 and returns `201` only after the object is readable with the expected checksum and metadata.
- Response loss followed by exact replay returns the same receipt with `200` and creates no second object.
- Concurrent exact duplicates yield one creation and safe replay responses; a same-ID content or metadata conflict yields `409` without overwrite.
- Wrong method/path/media type, malformed headers, digest mismatch, truncation, extra bytes, and bodies above 32 MiB fail without a durable object.
- Simulated R2 failure never returns 2xx.
- Worker-memory tests demonstrate that body handling does not buffer a full 32 MiB request.
- Deployment and log tests prove that the Worker has no R2 list/download/admin surface, no decrypt key, and no sensitive logging.
- Operational documentation explicitly describes ingress and all stored routing metadata as unauthenticated/untrusted.

## Documentation

Add the upload/receipt contract, Worker threat model, Wrangler deployment guide, R2 naming/metadata specification, WAF/rate-limit/lifecycle runbook, retention/cost guidance, researcher S3 read-access guide, and incident procedures.

References: [R2 Workers API](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/), [R2 consistency](https://developers.cloudflare.com/r2/reference/consistency/), and [Workers limits](https://developers.cloudflare.com/workers/platform/limits/).

## Dependencies

Blocked by #8 (final Protocol v1) and #9 (durable exact-replay Android outbox).

Contributor guide

Open the contributing guide

Research direction

Start by checking the blockers in #8 and #9, then read the referenced R2 Workers API, R2 consistency, and Workers limits documentation. Treat the acceptance criteria as the completion checklist: streaming, bounded validation, atomic idempotency, safe receipts, failure handling, deployment isolation, and non-sensitive logging must all be demonstrated.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, cloud, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.