Almacenamiento de archivos en buckets S3 compatibles (R2, S3, MinIO) y disco local, con subidas firmadas
Instalar
genpm add @core/storageQué obtienes
- Código en src/lib/storage/, 11 archivos. (22,7 kB)
- Reglas de IA en src/lib/storage/AGENTS.md, más archivos de reglas para tu IDE.
- Variables añadidas a .env.example: STORAGE_DRIVER, STORAGE_BUCKET, STORAGE_ENDPOINT, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY, STORAGE_PUBLIC_URL, STORAGE_SECRET, STORAGE_LOCAL_DIR.
README
Este paquete no tiene README.
Esto es exactamente lo que lee tu IA cuando trabaja en src/lib/storage. No se añade nada más a su contexto.
@core/storage — rules for AI agents
Purpose
One StorageProvider interface to store and serve files: S3-compatible buckets (Cloudflare R2, AWS S3, MinIO, B2)
signed with SigV4 (aws4fetch, works on Node and Workers), plus local (disk) and memory drivers for development
and tests. Signed direct uploads from the browser with the exact size and content type signed. No media library,
no image processing (that is @core/media). No tables.
Map
index.ts— public API:getStorage(),setStorage(),generateKey(),assertSafeKey(),s3Storage(), types.s3.ts— S3 driver.memory.ts— local/memory drivers and signed tokens.local-routes.ts— handlers for them.adapters/hono.ts,adapters/next.ts— upload/download route, only needed withlocal/memory.
Integration
- Env for S3/R2 (default driver):
STORAGE_ENDPOINT(R2:https://<account>.r2.cloudflarestorage.com),STORAGE_BUCKET,STORAGE_ACCESS_KEY_ID,STORAGE_SECRET_ACCESS_KEY, optionalSTORAGE_REGION(defaultauto) andSTORAGE_PUBLIC_URL(CDN/custom domain serving thepublic/prefix). Local dev:STORAGE_DRIVER=local,STORAGE_SECRET(≥ 32 chars),STORAGE_PUBLIC_URL=http://localhost:3000(files go toSTORAGE_LOCAL_DIR, default.storage/: add it to.gitignore), and mount the route: Honoapp.route('/api/storage', localStorageRoutes()); Next.jsapp/api/storage/[[...key]]/route.tswithexport { localStoragePut as PUT, localStorageGet as GET } from '@/lib/storage/adapters/next'. - Bucket CORS (S3/R2): allow
PUTfrom your origin with headercontent-type. - Direct upload: server creates
const key = generateKey('public/media', file.name)andawait getStorage().presignUpload(key, { contentType, size, maxBytes: 10_000_000 }); the browser doesfetch(url, { method: 'PUT', headers, body: file }). - Verify: upload a small file, then
getStorage().head(key)returns its size.
Conventions
- Keys are generated by the server (
generateKey); never use the user's filename or path as the key. - Everything is private unless its key starts with
public/; serve private files withpresignDownloadafter an authorization check. - Store the key in your table, not the URL (URLs change with the CDN or driver).
- A presigned upload URL can be reused until it expires. If the server validates or transforms the file after upload,
presign a private key (e.g.
uploads/pending/…) and have the server copy the validated bytes to the finalpublic/key.
Don't
- Don't make the whole bucket public or proxy private files without checking permissions.
- Don't trust the client's content type for security decisions; @core/media sniffs the real type after upload.
- Don't log storage credentials or presigned URLs.
# @core/storage — rules for AI agents
## Purpose
One `StorageProvider` interface to store and serve files: S3-compatible buckets (Cloudflare R2, AWS S3, MinIO, B2)
signed with SigV4 (`aws4fetch`, works on Node and Workers), plus `local` (disk) and `memory` drivers for development
and tests. Signed direct uploads from the browser with the exact size and content type signed. No media library,
no image processing (that is @core/media). No tables.
## Map
- `index.ts` — public API: `getStorage()`, `setStorage()`, `generateKey()`, `assertSafeKey()`, `s3Storage()`, types.
- `s3.ts` — S3 driver. `memory.ts` — local/memory drivers and signed tokens. `local-routes.ts` — handlers for them.
- `adapters/hono.ts`, `adapters/next.ts` — upload/download route, only needed with `local`/`memory`.
## Integration
1. Env for S3/R2 (default driver): `STORAGE_ENDPOINT` (R2: `https://<account>.r2.cloudflarestorage.com`),
`STORAGE_BUCKET`, `STORAGE_ACCESS_KEY_ID`, `STORAGE_SECRET_ACCESS_KEY`, optional `STORAGE_REGION` (default `auto`)
and `STORAGE_PUBLIC_URL` (CDN/custom domain serving the `public/` prefix).
Local dev: `STORAGE_DRIVER=local`, `STORAGE_SECRET` (≥ 32 chars), `STORAGE_PUBLIC_URL=http://localhost:3000`
(files go to `STORAGE_LOCAL_DIR`, default `.storage/`: add it to `.gitignore`), and
mount the route: Hono `app.route('/api/storage', localStorageRoutes())`; Next.js
`app/api/storage/[[...key]]/route.ts` with `export { localStoragePut as PUT, localStorageGet as GET } from '@/lib/storage/adapters/next'`.
2. Bucket CORS (S3/R2): allow `PUT` from your origin with header `content-type`.
3. Direct upload: server creates `const key = generateKey('public/media', file.name)` and
`await getStorage().presignUpload(key, { contentType, size, maxBytes: 10_000_000 })`; the browser does
`fetch(url, { method: 'PUT', headers, body: file })`.
4. Verify: upload a small file, then `getStorage().head(key)` returns its size.
## Conventions
- Keys are generated by the server (`generateKey`); never use the user's filename or path as the key.
- Everything is private unless its key starts with `public/`; serve private files with `presignDownload` after an authorization check.
- Store the key in your table, not the URL (URLs change with the CDN or driver).
- A presigned upload URL can be reused until it expires. If the server validates or transforms the file after upload,
presign a private key (e.g. `uploads/pending/…`) and have the server copy the validated bytes to the final `public/` key.
## Don't
- Don't make the whole bucket public or proxy private files without checking permissions.
- Don't trust the client's content type for security decisions; @core/media sniffs the real type after upload.
- Don't log storage credentials or presigned URLs.
El árbol exacto que se inyectará, tras aplicar .genpmignore. Anclado a
// Handlers de la ruta de almacenamiento local (drivers `local`/`memory`): subida firmada, descarga firmada y públicos.
import { assertSafeKey, isPublicKey, StorageError } from './keys.ts';
import { verifyGrant } from './memory.ts';
import type { StorageProvider } from './provider.ts';
const err = (status: number, error: string) => Response.json({ error }, { status });
/** PUT `<ruta>?token=…` con el archivo en el cuerpo. */
export async function handleLocalUpload(req: Request, storage: StorageProvider, secret: string): Promise<Response> {
try {
const g = await verifyGrant(secret, new URL(req.url).searchParams.get('token'));
if (g.op !== 'put') return err(403, 'invalid_token');
if (req.headers.get('content-type') !== g.type) return err(400, 'content_type_mismatch');
// Antes de leer el cuerpo: no aceptar más bytes de los firmados.
if (Number(req.headers.get('content-length') ?? g.size) !== g.size) return err(400, 'size_mismatch');
const bytes = new Uint8Array(await req.arrayBuffer());
if (bytes.byteLength !== g.size) return err(400, 'size_mismatch');
await storage.put(g.key, bytes, { contentType: g.type ?? 'application/octet-stream' });
return new Response(null, { status: 200 });
} catch (e) {
if (e instanceof StorageError) return err(403, e.code);
throw e;
}
}
/** GET `<ruta>?token=…` (privados) o `<ruta>/public/…` (públicos). */
export async function handleLocalDownload(req: Request, storage: StorageProvider, secret: string, routePath = '/api/storage'): Promise<Response> {
const url = new URL(req.url);
let key: string;
try {
if (url.searchParams.has('token')) {
const g = await verifyGrant(secret, url.searchParams.get('token'));
if (g.op !== 'get') return err(403, 'invalid_token');
key = g.key;
} else {
key = assertSafeKey(decodeURIComponent(url.pathname.slice(routePath.length + 1)));
if (!isPublicKey(key)) return err(404, 'not_found');
}
} catch (e) {
if (e instanceof StorageError) return err(e.code === 'invalid_key' ? 404 : 403, e.code);
throw e;
}
const o = await storage.get(key);
if (!o) return err(404, 'not_found');
return new Response(o.body, {
headers: {
'content-type': o.contentType,
'content-length': String(o.size),
'x-content-type-options': 'nosniff',
// Nunca se ejecuta como página del sitio (HTML/SVG subidos).
'content-security-policy': "default-src 'none'; sandbox",
},
});
}
Este paquete no declara servidores MCP.
| Versión | Commit | Publicado | Escaneo |
|---|---|---|---|
| 1.0.1 | 5b6a9cc | hace 5 horas | escaneo superado |
- genpm
- ninguno
- propuesta
- GenPM propone el comando npm y solo lo ejecuta si dices que sí.
- Usado por (1)
- @core/media ^1.0.0
- escaneo
- escaneo superado · 0 hallazgos
- commit
- v1.0.1 → 5b6a9cc5574ad638d71214af234145082f01cb98 · verificado tras la descarga
- scripts
- Ninguno. GenPM nunca ejecuta código del paquete.
- licencia
- MIT
- Calidad
- 100/100
- Licencia reconocidacumplido
- AGENTS.md explica su propósitocumplido
- AGENTS.md tiene pasos de integracióncumplido
- AGENTS.md lista convenciones o prohibicionescumplido
- Incluye testscumplido
- Escaneo de seguridad superadocumplido
- Publicado en los últimos 6 mesescumplido
- Publicador verificadocumplido
- Resumen y palabras clavecumplido
- reporte
- ¿Ves algo raro?