anolilab/lunora/lunora-setup-storage
Claude Code and agent skill by anolilab/lunora
- community
Checking that the SKILL.md file is still in the repository.
Adds R2-backed file storage to a Lunora app with the `storage` registry item (`@lunora/storage`). Covers worker-signed PUT/GET URLs, the `UPLOADS` R2 bucket binding, `STORAGE_SIGNING_SECRET` and `STORAGE_PUBLIC_BASE_URL`, per-user key scoping, and the `/storage/*` Worker route that verifies signatures and moves the bytes. Use when the user wants file or image uploads, avatars, attachments or gated downloads, runs `lunora add storage` or `lunora registry add storage`, edits `lunora/storage/index.ts`, or sees signed storage URLs return 404/403 or uploads fail CORS.
Install
npx vskill@latest install anolilab/lunora/lunora-setup-storageSKILL.md
Lunora Setup Storage
The storage registry item wraps @lunora/storage, an R2 adapter with HMAC signed-URL helpers. It exposes Lunora functions for browser uploads, gated downloads, delete and list. The client never holds a bucket credential.
A worker-signed URL points at your Worker, not at R2. Its shape is <base>/<key>?exp&method&bucket&sig. The base is ctx.origin unless STORAGE_PUBLIC_BASE_URL overrides it. The /storage/* route from Step 3 verifies the signature and moves the bytes, for both upload and download. @lunora/storage's getPresignedUrl takes the Worker out of the path, but it needs S3 credentials and none of your rules apply to it.
If the project has no Lunora backend yet, start with lunora-quickstart.
Step 1: Add the item
Then run pnpm install and lunora codegen. The item does the following:
- Adds
@lunora/storage,@lunora/server,@lunora/errorsand@lunora/ratelimit. - Merges an R2 binding named
UPLOADSintor2_bucketsinwrangler.jsonc. - Writes
STORAGE_SIGNING_SECRET(a secret) to.dev.vars, plus an empty, optionalSTORAGE_PUBLIC_BASE_URL. - Copies
lunora/storage/index.tsinto the project. The project owns this file. Codegen exposes its functions asapi.storage.generateUploadUrl,getDownloadUrlandlistObjects(actions) anddeleteObject(mutation). Every one requires a signed-in user, becauserequireOwnerthrowsUNAUTHORIZEDotherwise, so set up auth first (lunora-setup-auth). Each is rate-limited per user to 60 requests a minute through an in-memory, per-isolate limiter. For a durable limiter, runlunora add ratelimit.
Step 2: Configure the binding and secrets
| Name | Where | Notes | |
|---|---|---|---|
UPLOADS | wrangler.jsonc → r2_buckets | Set bucket_name to a real bucket: lowercase letters, digits and hyphens, 3–63 chars. Otherwise wrangler rejects it. | |
STORAGE_SIGNING_SECRET | secret | At least 32 chars; a shorter secret throws on the first call. Use openssl rand -base64 32, then wrangler secret put for production. | |
STORAGE_PUBLIC_BASE_URL | optional var | See below. |
Leave STORAGE_PUBLIC_BASE_URL empty for most apps. URLs are then signed against ctx.origin, which is correct in dev, in previews and in production. Set it only in two cases:
- Another host, such as a CDN, serves
/storage/*. - You sign where no request is behind the call: a query, a scheduled job or a workflow step. None of these has a
ctx.origin, so signing there throws.
The value must be a bare origin. The key is verified from the whole URL pathname, so the signer rejects a base that includes a path. Outside local dev the value must use https://, because a signed URL is a bearer credential.
Step 3: Add the /storage/* route to the Worker
The app needs this route. Without it, every minted URL falls through to the Lunora catch-all and returns 404. The route is also the only place the signature is checked, so a route that skips verifySignedUrl lets anyone read any key.
@lunora/server's serveStorageObject(ctx, key, request, authorize) covers the download half. It handles Range/206, ETag, nosniff and the inline-safe content-disposition list. It verifies nothing on its own, so call verifySignedUrl inside its authorize gate. It also doesn't handle uploads. Use it from an httpAction, where ctx.storage is in scope, when you need Range seeking.
The standalone route below is a plain Worker fetch that only has the R2 binding. It handles both verbs and serves whole objects:
CORS: if STORAGE_PUBLIC_BASE_URL is a different origin from the app, the browser preflights the PUT. The browser then reads access-control-allow-origin from the real response as well, which is why the 204 and the download response also spread ...cors. Keep the base same-origin and ALLOWED_ORIGINS can stay empty.
On a CDN or host-rewrite setup, pass { expectedHost } to verifySignedUrl, set to the host of STORAGE_PUBLIC_BASE_URL, so the signature is checked against the host the URL was minted for.
Step 4: Upload and download from the client
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingpackages/cli/skills/lunora-setup-storage/SKILL.md
Popularity
- 7-day trend score
- 5
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/anolilab/lunora/lunora-setup-storage)