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.
git clone --depth 1 https://github.com/glitternetwork/pinme /tmp/pinme-r2 && cp -r /tmp/pinme-r2/skills/pinme-r2 ~/.claude/skills/pinme-r2SKILL.md
# 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:
```typescript
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:
```text
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:
```typescript
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.
```typescript
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.
```typescript
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 requeUse when a PinMe project (Worker TypeScript) needs to integrate user authentication — creating email/password users, verifying id_tokens, querying user info, or listing users via Identity Platform auth proxy APIs.
Use this skill when a PinMe project (Worker TypeScript) needs to integrate email sending (send_email). Guides AI to generate correct Worker TS code.
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.
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`.
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.
Use when generating, modifying, or reviewing PinMe Worker (Cloudflare Worker TypeScript) code that accepts payments through UniwebPay — payment links, products/prices, checkout sessions, payment status reads, refunds, subscriptions, or handling UniwebPay webhooks with @uniwebpay/sdk in a PinMe project.