$npx -y skills add glitternetwork/pinme --skill pinme-r2Use 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
| 1 | # PinMe Worker R2 Storage |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Runtime Contract |
| 6 | |
| 7 | PinMe rebuilds trusted Worker metadata on create, save, and update. Client metadata cannot replace the R2 binding. |
| 8 | |
| 9 | | Binding | TypeScript type | Availability | |
| 10 | | --- | --- | --- | |
| 11 | | `DB` | `D1Database` | Always injected | |
| 12 | | `R2` | `R2Bucket` | Always injected; current project's bucket | |
| 13 | | `API_KEY` | `string` | Always injected | |
| 14 | | `LLM_API_KEY` | `string` | Always injected | |
| 15 | | `BASE_URL` | `string` | Always injected | |
| 16 | | `WORKER_URL` | `string` | Always injected | |
| 17 | | `PROJECT_NAME` | `string` | Always injected | |
| 18 | |
| 19 | Payment-specific bindings such as `UNIWEB_SECRET` are conditional and unrelated to R2 access. |
| 20 | |
| 21 | Declare only the bindings used by the Worker module. R2 code normally starts with: |
| 22 | |
| 23 | ```typescript |
| 24 | export interface Env { |
| 25 | R2: R2Bucket; |
| 26 | PROJECT_NAME: string; |
| 27 | WORKER_URL: string; |
| 28 | } |
| 29 | ``` |
| 30 | |
| 31 | When the same module coordinates file metadata in D1, also declare `DB: D1Database` as a required field. |
| 32 | |
| 33 | ## Choose R2 or D1 |
| 34 | |
| 35 | - Use R2 for file bodies, images, attachments, media, exports, and other objects addressed by key. |
| 36 | - Use D1 for searchable business metadata, ownership, relations, status, and audit fields. |
| 37 | - For managed files, store the body in R2 and store only its key and business metadata in D1. |
| 38 | - Never use Worker local filesystem state for persistence and never store complete files or base64 payloads in D1. |
| 39 | |
| 40 | ## Required Security Workflow |
| 41 | |
| 42 | Apply this sequence to every upload, download, metadata, delete, and list route: |
| 43 | |
| 44 | ```text |
| 45 | authenticate request |
| 46 | → authorize the project/user action |
| 47 | → validate size and media policy |
| 48 | → generate or normalize a scoped object key |
| 49 | → call env.R2 |
| 50 | → return a sanitized response |
| 51 | ``` |
| 52 | |
| 53 | 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. |
| 54 | |
| 55 | Keep object keys server-controlled. Prefer opaque IDs under an owner prefix: |
| 56 | |
| 57 | ```typescript |
| 58 | 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; |
| 59 | |
| 60 | function ownerPrefix(userId: string): string { |
| 61 | if (!userId) throw new Error('Authenticated user id is required'); |
| 62 | return `users/${encodeURIComponent(userId)}/files/`; |
| 63 | } |
| 64 | |
| 65 | function objectKey(userId: string, fileId: string): string { |
| 66 | if (!FILE_ID_RE.test(fileId)) throw new Error('Invalid file id'); |
| 67 | return `${ownerPrefix(userId)}${fileId}`; |
| 68 | } |
| 69 | ``` |
| 70 | |
| 71 | 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. |
| 72 | |
| 73 | ## Shared Helpers |
| 74 | |
| 75 | Use small helpers and explicit business limits. Adapt the allowlist to the product rather than accepting every client-supplied media type. |
| 76 | |
| 77 | ```typescript |
| 78 | const MAX_UPLOAD_BYTES = 25 * 1024 * 1024; |
| 79 | const ALLOWED_CONTENT_TYPES = new Set([ |
| 80 | 'image/jpeg', |
| 81 | 'image/png', |
| 82 | 'image/webp', |
| 83 | 'application/pdf', |
| 84 | ]); |
| 85 | |
| 86 | function json(data: unknown, status = 200): Response { |
| 87 | return Response.json(data, { status }); |
| 88 | } |
| 89 | |
| 90 | function safeDownloadName(value: string | null): string { |
| 91 | const cleaned = (value || 'download') |
| 92 | .replace(/[\r\n"\\]/g, '_') |
| 93 | .replace(/[\x00-\x1f\x7f]/g, '') |
| 94 | .trim(); |
| 95 | return (cleaned || 'download').slice(0, 128); |
| 96 | } |
| 97 | |
| 98 | function requestedFileId(request: Request): string | null { |
| 99 | const url = new URL(request.url); |
| 100 | const value = url.pathname.split('/').filter(Boolean).at(-1) || ''; |
| 101 | return FILE_ID_RE.test(value) ? value : null; |
| 102 | } |
| 103 | ``` |
| 104 | |
| 105 | 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. |
| 106 | |
| 107 | ## Stream an Upload |
| 108 | |
| 109 | Require authentication before calling this handler. Pass `request.body` directly to R2; do not call `arrayBuffer()`, `text()`, `json()`, `formData()`, or base64 conversion first. |
| 110 | |
| 111 | ```typescript |
| 112 | async function handleUpload( |
| 113 | request: Request, |
| 114 | env: Env, |
| 115 | userId: string, |
| 116 | ): Promise<Response> { |
| 117 | if (!request.body) return json({ error: 'File body is required' }, 400); |
| 118 | |
| 119 | const lengthHeader = request.headers.get('content-length'); |
| 120 | if (!lengthHeader) return json({ error: 'Content-Length is required' }, 411); |
| 121 | |
| 122 | const declaredSize = Number(lengthHeader); |
| 123 | if (!Number.isSafeInteger(declaredSize) || declaredSize < 0) { |
| 124 | return json({ error: 'Invalid Content-Length' }, 400); |
| 125 | } |
| 126 | if (declaredSize > MAX_UPLOAD_BYTES) { |
| 127 | return json( |