
pinme-r2
Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination. Guides AI to use PinMe's automatically injected env.R2 binding without R2 credentials or manual Wrangler configuration.
- Installs
- 83
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/glitternetwork/pinme --skill pinme-r2- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads pinme-r2 automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
PinMe Worker R2 Storage
Use the project-scoped R2 bucket that PinMe binds to every deployed Worker as env.R2. Do not create credentials, choose a bucket name, or edit generated Wrangler configuration.
Runtime Contract
PinMe rebuilds trusted Worker metadata on create, save, and update. Client metadata cannot replace the R2 binding.
| Binding | TypeScript type | Availability |
|---|---|---|
DB | D1Database | Always injected |
R2 | R2Bucket | Always injected; current project's bucket |
API_KEY | string | Always injected |
LLM_API_KEY | string | Always injected |
BASE_URL | string | Always injected |
WORKER_URL | string | Always injected |
PROJECT_NAME | string | Always injected |
Payment-specific bindings such as UNIWEB_SECRET are conditional and unrelated to R2 access.
Declare only the bindings used by the Worker module. R2 code normally starts with:
export interface Env {
R2: R2Bucket;
PROJECT_NAME: string;
WORKER_URL: string;
}
When the same module coordinates file metadata in D1, also declare DB: D1Database as a required field.
Choose R2 or D1
- Use R2 for file bodies, images, attachments, media, exports, and other objects addressed by key.
- Use D1 for searchable business metadata, ownership, relations, status, and audit fields.
- For managed files, store the body in R2 and store only its key and business metadata in D1.
- Never use Worker local filesystem state for persistence and never store complete files or base64 payloads in D1.
Required Security Workflow
Apply this sequence to every upload, download, metadata, delete, and list route:
authenticate request
→ authorize the project/user action
→ validate size and media policy
→ generate or normalize a scoped object key
→ call env.R2
→ return a sanitized response
Use the application's existing authentication. The examples below accept a trusted userId that the route must obtain from verified identity claims, never from an untrusted request body or query parameter.
Keep object keys server-controlled. Prefer opaque IDs under an owner prefix:
const FILE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function ownerPrefix(userId: string): string {
if (!userId) throw new Error('Authenticated user id is required');
return `users/${encodeURIComponent(userId)}/files/`;
}
function objectKey(userId: string, fileId: string): string {
if (!FILE_ID_RE.test(fileId)) throw new Error('Invalid file id');
return `${ownerPrefix(userId)}${fileId}`;
}
Never accept a complete object key from the client. Reject empty identifiers, . or .. segments, backslashes, control characters, and any attempt to access another user's prefix.
Shared Helpers
Use small helpers and explicit business limits. Adapt the allowlist to the product rather than accepting every client-supplied media type.
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
const ALLOWED_CONTENT_TYPES = new Set([
'image/jpeg',
'image/png',
'image/webp',
'application/pdf',
]);
function json(data: unknown, status = 200): Response {
return Response.json(data, { status });
}
function safeDownloadName(value: string | null): string {
const cleaned = (value || 'download')
.replace(/[\r\n"\\]/g, '_')
.replace(/[\x00-\x1f\x7f]/g, '')
.trim();
return (cleaned || 'download').slice(0, 128);
}
function requestedFileId(request: Request): string | null {
const url = new URL(request.url);
const value = url.pathname.split('/').filter(Boolean).at(-1) || '';
return FILE_ID_RE.test(value) ? value : null;
}
Client filenames and Content-Type are hints, not proof of content. For sensitive formats, inspect magic bytes or send the object through an asynchronous validation/scanning workflow before marking it ready.
Stream an Upload
Require authentication before calling this handler. Pass request.body directly to R2; do not call arrayBuffer(), text(), json(), formData(), or base64 conversion first.
async function handleUpload(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
if (!request.body) return json({ error: 'File body is required' }, 400);
const lengthHeader = request.headers.get('content-length');
if (!lengthHeader) return json({ error: 'Content-Length is required' }, 411);
const declaredSize = Number(lengthHeader);
if (!Number.isSafeInteger(declaredSize) || declaredSize < 0) {
return json({ error: 'Invalid Content-Length' }, 400);
}
if (declaredSize > MAX_UPLOAD_BYTES) {
return json({ error: 'File is too large' }, 413);
}
const contentType = (request.headers.get('content-type') || '')
.split(';', 1)[0]
.trim()
.toLowerCase();
if (!ALLOWED_CONTENT_TYPES.has(contentType)) {
return json({ error: 'Unsupported media type' }, 400);
}
const fileId = crypto.randomUUID();
const key = objectKey(userId, fileId);
const filename = safeDownloadName(request.headers.get('x-file-name'));
const object = await env.R2.put(key, request.body, {
httpMetadata: {
contentType,
contentDisposition: `attachment; filename="${filename}"`,
},
customMetadata: { ownerId: userId },
});
if (object === null) return json({ error: 'Upload precondition failed' }, 412);
// Content-Length is only a precheck. Enforce the actual stored size too.
if (object.size > MAX_UPLOAD_BYTES) {
await env.R2.delete(key);
return json({ error: 'File is too large' }, 413);
}
return json({ id: fileId, size: object.size, etag: object.httpEtag }, 201);
}
Do not return the bucket name or internal object-key layout. Return an opaque file ID that later routes resolve under the authenticated owner's prefix.
Stream a Download
Validate a single Range header before passing it to R2. R2 may return null when the object does not exist, or metadata without a body when a conditional request fails.
function validRangeHeader(value: string | null): boolean {
if (!value) return true;
const match = /^bytes=(\d*)-(\d*)$/.exec(value);
return Boolean(match && (match[1] || match[2]));
}
async function handleDownload(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const fileId = requestedFileId(request);
if (!fileId) return json({ error: 'Invalid file id' }, 400);
if (!validRangeHeader(request.headers.get('range'))) {
return json({ error: 'Invalid Range header' }, 416);
}
const object = await env.R2.get(objectKey(userId, fileId), {
onlyIf: request.headers,
range: request.headers,
});
if (object === null) return json({ error: 'Not found' }, 404);
if (!('body' in object)) return new Response(null, { status: 412 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('accept-ranges', 'bytes');
if (object.range) {
const { offset, length } = object.range;
headers.set(
'content-range',
`bytes ${offset}-${offset + length - 1}/${object.size}`,
);
headers.set('content-length', String(length));
} else {
headers.set('content-length', String(object.size));
}
return new Response(object.body, {
status: object.range ? 206 : 200,
headers,
});
}
For routes backed by D1 metadata, authorize the D1 row's owner before calling env.R2.get. Do not infer ownership only from a client-provided path.
Read Metadata with HEAD
async function handleHead(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const fileId = requestedFileId(request);
if (!fileId) return json({ error: 'Invalid file id' }, 400);
const object = await env.R2.head(objectKey(userId, fileId));
if (object === null) return new Response(null, { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('content-length', String(object.size));
return new Response(null, { status: 200, headers });
}
Use head() when only size, ETag, upload time, or metadata is needed. Do not download the body to answer metadata requests.
Delete an Object
async function handleDelete(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const fileId = requestedFileId(request);
if (!fileId) return json({ error: 'Invalid file id' }, 400);
const key = objectKey(userId, fileId);
const object = await env.R2.head(key);
if (object === null) return json({ error: 'Not found' }, 404);
await env.R2.delete(key);
return new Response(null, { status: 204 });
}
R2 can delete up to 1000 keys in one delete([...keys]) call. Batch deletion must still derive and authorize every key server-side.
List an Owner's Objects
Never list the whole bucket for an end-user request. Derive the prefix from verified identity and treat the cursor as opaque.
async function handleList(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const cursor = new URL(request.url).searchParams.get('cursor');
if (cursor && cursor.length > 2048) {
return json({ error: 'Invalid cursor' }, 400);
}
const page = await env.R2.list({
prefix: ownerPrefix(userId),
cursor: cursor || undefined,
limit: 100,
include: ['httpMetadata', 'customMetadata'],
});
return json({
objects: page.objects.map((object) => ({
id: object.key.slice(ownerPrefix(userId).length),
size: object.size,
uploaded: object.uploaded.toISOString(),
etag: object.httpEtag,
contentType: object.httpMetadata?.contentType,
})),
nextCursor: page.truncated ? page.cursor : null,
});
}
An R2 list call returns at most 1000 entries and may return fewer than the requested limit when metadata is included. Continue only when page.truncated is true; never use objects.length === limit as the pagination condition.
Route and Error Semantics
Authenticate once in the router, derive a trusted userId, then pass it to the handlers. Return an Allow header for unsupported methods.
| Status | Meaning |
|---|---|
| 400 | Invalid file ID, body, cursor, or media type |
| 401 | Missing or invalid authentication |
| 403 | Authenticated but not allowed to access the object |
| 404 | Object or owned metadata record not found |
| 411 | A capped upload route requires Content-Length but it is absent |
| 412 | Conditional R2 operation failed |
| 413 | Business or platform upload limit exceeded |
| 416 | Invalid or unsatisfiable Range request |
| 500 | Sanitized internal storage failure |
Catch storage failures at the route boundary, log only non-sensitive context, and return a generic error. Never return a raw provider error, bucket name, credential, or internal object key.
Translate a valid-but-unsatisfiable R2 Range failure to 416 without returning the provider error text.
Coordinate R2 with D1
R2 and D1 do not share a transaction. Use an explicit state transition when business metadata is required:
insert D1 row with status=pending
→ stream body to R2
→ update D1 row to status=ready
- If upload fails, delete the pending row or mark it failed.
- If the final D1 update fails, delete the newly uploaded object or retain a durable pending state for a compensation job.
- Store at least: public file ID, internal object key, owner ID, original name, size, MIME, status, and timestamps.
- For download and delete, load the row by public file ID and owner ID before touching R2.
- Delete the R2 object and D1 row with an explicit retry/compensation policy; do not pretend the two operations are atomic.
Large Files
Use request.body → env.R2.put for small and medium uploads. Streaming avoids Worker memory amplification but does not bypass the Cloudflare request-body limit for the account plan.
Use multipart only when the object exceeds that request limit or resumability is an explicit product requirement. A multipart API must:
- authenticate every create, upload-part, complete, resume, and abort action;
- bind the object key and upload ID to an owner in durable state;
- validate part number, part size, total size, and declared content type;
- make completion idempotent and abort stale uploads;
- avoid accepting an arbitrary key or upload ID from an untrusted client.
Do not generate a public multipart controller by default. Multipart state and security are substantially more complex than a single streaming upload.
Local Development
- Do not edit PinMe-generated
backend/wrangler.tomlto add an R2 binding. - Unit-test key generation, authorization, routing, and failure handling with a narrow
R2Bucketmock. - Verify real metadata, Range, conditional requests, and streaming after
pinme update-workerorpinme save. - Treat the mock as a logic test, not proof of production R2 behavior.
Anti-Patterns
| Do not | Use instead |
|---|---|
| Expose an unauthenticated upload route | Authenticate and authorize before every mutation |
| Accept a complete object key from the client | Generate an opaque ID under a server-derived owner prefix |
| Trust a user ID from JSON or query parameters | Derive identity from verified claims |
| Read a large body into an ArrayBuffer or base64 string | Stream request.body directly into env.R2.put |
| Store file bodies or base64 in D1 | Store bodies in R2 and searchable metadata in D1 |
| List the whole bucket | Restrict with an owner prefix and paginate |
| Stop pagination based on returned object count | Check page.truncated and return page.cursor |
| Drop response metadata | Apply writeHttpMetadata, httpEtag, length, and Range headers |
Persist with fs or local directories | Use the injected R2 binding |
| Add R2 keys or secrets to source/config | Use env.R2; PinMe owns the binding |
| Edit generated Wrangler binding configuration | Deploy through pinme save or pinme update-worker |
FAQ
What does the pinme-r2 skill do?
Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination. Guides AI to use PinMe's automatically injected env.R2 binding without R2 credentials or manual Wrangler configuration.
How do I install pinme-r2?
Run npx skills add https://github.com/glitternetwork/pinme --skill pinme-r2 in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/pinme-r2/ (personal) or .claude/skills/pinme-r2/ (project).
Which AI agents can use pinme-r2?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is pinme-r2 safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains pinme-r2?
It is published by glitternetwork in glitternetwork/pinme. It has 83 recorded installs.
More from glitternetwork/pinme
pinme
glitternetwork/pinme
Use this skill when the user mentions "pinme", or needs to upload files, store to IPFS, create/publish/deploy websites or full-stack services (including frontend pages, backend APIs, database storage, email sending, etc.), or any feature requiring backend database/server support.
pinme-auth
glitternetwork/pinme
Use when a PinMe project (Worker TypeScript) needs to integrate user authentication — creating email/password users, verifying idtokens, querying user info, or listing users via Identity Platform auth proxy APIs.
pinme-share
glitternetwork/pinme
Use this skill when the user wants to share, publish, or upload a static result through PinMe, especially by generating a static HTML share page for a PinMe project link, deployed full-stack app, Codex conversation summary, report, file, demo, or any 分享/发布/上传分享页 request that should end with pinme upload.
pinme-email
glitternetwork/pinme
Use this skill when a PinMe project (Worker TypeScript) needs to integrate email sending (sendemail). Guides AI to generate correct Worker TS code.
pinme-llm
glitternetwork/pinme
Use this skill when a PinMe project (Worker TypeScript) needs to call OpenRouter-backed LLM APIs, including models, chat/completions, streaming, or OpenRouter web search. Guides AI to generate correct Worker TS code.
Popular devops & cloud skills
microsoft-foundry
microsoft/azure-skills
Build, deploy, evaluate, optimize, fine-tune, and manage Microsoft Foundry agents, models, and resources end to end. USE FOR: foundry, azd ai agent, azd provision/deploy, hosted agent scaffold/develop/run/deploy/troubleshoot, prompt agent create, managed harness agent create, GitHub Copilot harness agent, create agent, update agent, add tool to agent, invoke agent, agent insights, evaluate agent, batch eval, continuous eval, continuous monitoring, agent CI/CD, optimize prompt, improve prompt, optimize agent instructions, Agent Optimizer scaffold, dataset curation from traces, deploy model, model fine-tuning (SFT/DPO/RFT), Foundry project, RBAC, role assignment, permissions, quota, capacity, region, deployment failure, AI Services, create Foundry resource, knowledge index, customize deployment, onboard, availability, training-data, grader, distillation, large file upload. DO NOT USE FOR: Azure Functions, App Service, general Azure deploy (use azure-deploy), general Azure prep (use azure
azure-diagnostics
microsoft/azure-skills
Debug Azure production issues on Azure using AppLens, Azure Monitor, resource health, and safe triage. WHEN: debug production issues, troubleshoot app service, app service high CPU, app service deployment failure, troubleshoot container apps, container app 503 or target port mismatch, troubleshoot functions, function telemetry missing or stopped after a config/connection-string change, troubleshoot AKS, VM RDP, Linux SSH, VM black screen, can't connect to VM, reset VM password, NSG or firewall blocking, kubectl cannot connect, kube-system/CoreDNS failures, pod pending, crashloop, node not ready, upgrade failures, analyze logs, KQL, insights, image pull failures, cold start issues, health probe failures, resource health, root cause of errors, troubleshoot event hubs, troubleshoot service bus, messaging SDK error, AMQP connection failure, message lock lost, lock renewal during long-running processing, service bus dead letter.
azure-prepare
microsoft/azure-skills
Prepare azd-based Azure projects for deployment: generates azure.yaml, infrastructure (Bicep/Terraform), and Dockerfiles for the Azure Developer CLI (azd) workflow. USE ONLY when the user explicitly wants to use azd as the deployment tool, or the project already has an azure.yaml file. DO NOT USE FOR: non-azd deployments, Python App Service code-only deploys (use python-appservice-deploy), or cross-cloud migration (use azure-cloud-migrate). WHEN: prepare app for azd, create azure.yaml, set up azd infrastructure, modernize app for Azure with azd, deploy with azd, function app, timer trigger, service bus trigger, event-driven function, managed identity, generate Bicep, generate Terraform, create and deploy to Azure.
azure-ai
microsoft/azure-skills
Use for Azure AI: Search, Speech, OpenAI, Document Intelligence. Helps with search, vector/hybrid search, speech-to-text, text-to-speech, transcription, OCR. WHEN: AI Search, query search, vector search, hybrid search, semantic search, speech-to-text, text-to-speech, transcribe, OCR, convert text to speech.
azure-deploy
microsoft/azure-skills
Execute Azure deployments for ALREADY-PREPARED applications that have existing .azure/deployment-plan.md and infrastructure files. DO NOT use this skill when the user asks to CREATE a new application — use azure-prepare instead. This skill runs azd up, azd deploy, terraform apply, and az deployment commands with built-in error recovery. Requires .azure/deployment-plan.md from azure-prepare and validated status from azure-validate. WHEN: "run azd up", "run azd deploy", "execute deployment", "push to production", "push to cloud", "go live", "ship it", "bicep deploy", "terraform apply", "publish to Azure", "launch on Azure". DO NOT USE WHEN: "create and deploy", "build and deploy", "create a new app", "set up infrastructure", "create and deploy to Azure using Terraform" — use azure-prepare for these.
azure-validate
microsoft/azure-skills
Pre-deployment validation for Azure readiness. Run deep checks on configuration, infrastructure (Bicep or Terraform), RBAC role assignments, managed identity permissions, and prerequisites before deploying. WHEN: validate my app, check deployment readiness, run preflight checks, verify configuration, check if ready to deploy, validate azure.yaml, validate Bicep, test before deploying, troubleshoot deployment errors, validate Azure Functions, validate function app, validate serverless deployment, verify RBAC roles, check role assignments, review managed identity permissions, what-if analysis, validate Container Apps deployment.