A lightweight Cloudflare Worker that proxies a Cloudflare R2 bucket as
an image CDN at img.mrdemonwolf.com. It handles serving, uploading,
and deleting images with strict CORS controls, full SigV4 authentication
for writes, and aggressive edge caching for reads. Built for Shottr
screenshot uploads — fast delivery, zero infrastructure overhead.
Keep your screenshots hosted. Keep your CDN simple.
- R2 proxying — Serves objects directly from a bound R2 bucket with
correct
Content-Typeheaders and branded 404 pages. - Aggressive caching — Served objects carry
Cache-Control: public, max-age=31536000, immutablefor full edge and browser caching. - Full SigV4 authentication —
PUTandDELETErequire a valid AWS Signature V4Authorizationheader.GETsupports presigned URLs with expiry and signature verification via Web Crypto. - CORS enforcement — Restricts
Access-Control-Allow-Origintomrdemonwolf.comandwww.mrdemonwolf.comonly. - Key normalisation — Accepts paths with or without the bucket-name
prefix (
/shottr/foo.pngand/foo.pngresolve to the same object). - S3 compatibility — Returns S3 XML error responses, a stub
ListBucketResultonGET /, and aLocationConstraintonGET /?locationso S3 clients pass their connection test. - Delete support — Authenticated
DELETErequests remove objects from R2.
- Clone the repository.
git clone https://github.com/mrdemonwolf/shottr.git cd shottr - Install dependencies.
npm install
- Set required secrets.
npx wrangler secret put S3_ACCESS_KEY_ID npx wrangler secret put S3_SECRET_ACCESS_KEY
- Deploy.
npm run deploy
Serve an image (public, no auth required):
GET https://img.mrdemonwolf.com/screenshot.png
Upload via curl (SigV4 header required):
curl -X PUT https://img.mrdemonwolf.com/screenshot.png \
-H "Authorization: AWS4-HMAC-SHA256 Credential=<key>/..." \
-H "Content-Type: image/png" \
--data-binary @screenshot.pngPresigned GET (time-limited, no credentials in request):
GET https://img.mrdemonwolf.com/screenshot.png
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=...
&X-Amz-Date=...
&X-Amz-Expires=3600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=...
Any S3-compatible client (including Shottr's built-in S3 upload) can
point at https://img.mrdemonwolf.com with the configured key pair and
use it as a standard S3 bucket.
| Layer | Technology |
|---|---|
| Runtime | Cloudflare Workers |
| Storage | Cloudflare R2 |
| Language | TypeScript (strict, ESNext) |
| Auth | AWS Signature V4 (Web Crypto) |
| Deployment | Wrangler CLI |
| Testing | Vitest |
| Type defs | @cloudflare/workers-types |
- Node.js 20+
- Wrangler CLI (
npm installinstalls it locally) - A Cloudflare account with an R2 bucket named
shottr
- Install dependencies.
npm install
- Set secrets for local dev (write to
.dev.vars).echo "S3_ACCESS_KEY_ID=your_key" >> .dev.vars echo "S3_SECRET_ACCESS_KEY=your_secret" >> .dev.vars
- Start the local dev server.
npm run dev
npm run dev— Runs the worker locally viawrangler dev.npm run deploy— Builds and deploys the worker to Cloudflare.npm run typecheck— Runstsc --noEmitfor type checking only.npm run test— Runs the Vitest test suite.npm run build— Dry-run deploy todist/without publishing.
- TypeScript strict mode (
noEmit,strict,ESNexttarget). @cloudflare/workers-typesfor accurate R2 and Workers type definitions.- Vitest for unit tests including SigV4 verification logic.
.
├── src/
│ ├── index.ts # Worker entry point — all HTTP request handling
│ └── sigv4.ts # SigV4 header + presigned URL verification
├── wrangler.toml # Cloudflare Worker config, routes, R2 binding
├── tsconfig.json # TypeScript compiler options
└── package.json # Scripts and dev dependencies
For questions or feedback:
- Discord: Join my server
Made with love by MrDemonWolf, Inc.