pinme-auth
Use when a PinMe project (Worker TypeScript) needs to integrate user authentication — creating email/password users, verifying…
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
Claude Code
$ npx -y skills add glitternetwork/pinme --skill pinme-r2 --agent claude-code
Codex
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a codex -y
Cursor
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a cursor -y
Gemini CLI
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a gemini-cli -y
GitHub Copilot
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a github-copilot -y
OpenCode
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a opencode -y
Windsurf
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a windsurf -y
name: pinme-r2 description: 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.
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.
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.
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.
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.
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 }PinMe is a zero-config deployment CLI focused on one-command creation and deployment for full-stack projects. It lets you quickly set up and launch a complete project with an integrated frontend, Worker backend, and database, without tedious configuration.
Use when a PinMe project (Worker TypeScript) needs to integrate user authentication — creating email/password users, verifying…
Use this skill when a PinMe project (Worker TypeScript) needs to integrate email sending (send_email). Guides AI to generate…
Use this skill when a PinMe project (Worker TypeScript) needs to call OpenRouter-backed LLM APIs, including models,…
Use this skill when the user wants to share, publish, or upload a static result through PinMe, especially by generating a static…
Use when generating, modifying, or reviewing PinMe Worker (Cloudflare Worker TypeScript) code that accepts payments through…
Use this skill when the user mentions "pinme", or needs to upload files, store to IPFS, create/publish/deploy websites or…
Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing…
Use when launching cloud VMs, Kubernetes pods, or Slurm jobs for GPU/TPU/CPU workloads, training or fine-tuning models on cloud…