Lana Media Server is an authenticated file hosting service at
https://media.lanaloves.us. Upload images and documents with a secp256k1 signature,
receive a permanent public URL that works forever — no auth needed to read.
https://media.lanaloves.us10 MB50 MB
Every POST /api/upload must carry three HTTP headers. Reading (GET) requires no auth.
| Header | Value | Notes |
|---|---|---|
X-Upload-Pubkey | 64-char hex | secp256k1 public key — x-coordinate only (Nostr / LanaCoin style) |
X-Upload-Timestamp | Unix seconds | Must be within ±5 minutes of server time |
X-Upload-Sig | Hex DER ECDSA signature | Signs the message below |
Message being signed (hash input):
"lana-media-upload:" + timestamp_as_string → SHA-256 → sign with ECDSA secp256k1
Upload an image or document. Returns the permanent public URL.
multipart/form-datafile required{
"filename": "3f8a1c2d-…uuid.jpg",
"url": "https://media.lanaloves.us/images/3f8a1c2d-….jpg",
"width": 1024,
"height": 768,
"size": 48210,
"mime_type": "image/jpeg",
"category": "image"
}
{
"filename": "7b3e9f12-…uuid.pdf",
"url": "https://media.lanaloves.us/files/7b3e9f12-….pdf",
"size": 204800,
"mime_type": "application/pdf",
"category": "document"
}
| Status | Meaning |
|---|---|
400 | Missing headers, invalid pubkey format, unsupported type, or invalid magic bytes |
403 | Invalid signature or timestamp outside ±5 min window |
413 | File too large — images max 10 MB, documents max 50 MB |
500 | Processing or write error (corrupt file) |
Serve an uploaded image. No auth — fully public.
Cached 1 year (Cache-Control: immutable). Served directly by nginx from SSD on hot tier.
Serve an uploaded document. No auth — fully public.
Returns the correct Content-Type for each extension. Cached 1 year.
https://media.lanaloves.us/images/<uuid>.jpg and
https://media.lanaloves.us/files/<uuid>.ext are
accessible to anyone without any key or token. Embed in HTML, Nostr events,
mobile apps, or any third-party service. URLs never change across storage tiers.
Storage statistics. No auth required.
{
"total_files": 55,
"total_images": 42,
"total_documents": 13,
"total_bytes": 18432100,
"hot_count": 12,
"warm_count": 20,
"cold_count": 23,
"disk_free_bytes": 119000000000
}
Works in any being or Node.js backend. Only requires elliptic (already on all Lana servers).
const crypto = require('crypto'); const { ec: EC } = require('elliptic'); const https = require('https'); const fs = require('fs'); const ec = new EC('secp256k1'); /** Upload any file. wifOrPrivHex = LanaCoin WIF or 64-char hex private key. */ async function uploadFile(fileBuffer, filename, mimeType, wifOrPrivHex) { const privHex = wifOrPrivHex.length === 64 ? wifOrPrivHex : await wifToPrivHex(wifOrPrivHex); const keyPair = ec.keyFromPrivate(privHex); const pubHex = keyPair.getPublic().getX().toString('hex').padStart(64, '0'); const ts = Math.floor(Date.now() / 1000); const msgHash = crypto.createHash('sha256').update('lana-media-upload:' + ts).digest(); const sig = keyPair.sign(msgHash).toDER('hex'); const boundary = 'LanaMedia' + Date.now(); const CRLF = ' '; const body = Buffer.concat([ Buffer.from(`--${boundary}${CRLF}`), Buffer.from(`Content-Disposition: form-data; name="file"; filename="${filename}"${CRLF}`), Buffer.from(`Content-Type: ${mimeType}${CRLF}${CRLF}`), fileBuffer, Buffer.from(`${CRLF}--${boundary}--${CRLF}`), ]); return new Promise((resolve, reject) => { const req = https.request({ hostname: 'media.lanaloves.us', path: '/api/upload', method: 'POST', headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': body.length, 'X-Upload-Pubkey': pubHex, 'X-Upload-Timestamp': String(ts), 'X-Upload-Sig': sig, }, }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { const json = JSON.parse(d); res.statusCode === 200 ? resolve(json) : reject(new Error(json.error)); }); }); req.on('error', reject); req.write(body); req.end(); }); } // Upload an image uploadFile(fs.readFileSync('photo.jpg'), 'photo.jpg', 'image/jpeg', process.env.NOSTR_NSEC) .then(r => console.log('Image URL:', r.url)); // Upload a PDF uploadFile(fs.readFileSync('report.pdf'), 'report.pdf', 'application/pdf', process.env.NOSTR_NSEC) .then(r => console.log('Document URL:', r.url));
Paste this into any AI system prompt to enable file uploads:
You have access to a file upload service at https://media.lanaloves.us
POST https://media.lanaloves.us/api/upload (multipart/form-data, field: "file")
Required headers:
X-Upload-Pubkey: <64-char hex secp256k1 public key (x-coord only)>
X-Upload-Timestamp: <current Unix timestamp in seconds>
X-Upload-Sig: <hex DER ECDSA signature>
Signature: ECDSA_secp256k1_sign( SHA256("lana-media-upload:" + timestamp) )
Timestamp must be within ±5 min of server time. Generate fresh per request.
Accepted types:
Images (max 10 MB): image/jpeg, image/png, image/webp, image/gif
→ output always JPEG, max 1200px, quality 80
Documents (max 50 MB): application/pdf,
Word (.docx/.doc), Excel (.xlsx/.xls), PowerPoint (.pptx/.ppt),
OpenDocument (.odt/.ods/.odp), text/plain, text/csv
Success (HTTP 200):
Images: { "url": "https://media.lanaloves.us/images/<uuid>.jpg",
"filename", "width", "height", "size", "mime_type", "category": "image" }
Documents: { "url": "https://media.lanaloves.us/files/<uuid>.<ext>",
"filename", "size", "mime_type", "category": "document" }
All returned URLs are permanent and publicly accessible without authentication.
| Tier | Location | Age | Served by |
|---|---|---|---|
| hot | Local SSD storage/hot/ | 0 – 30 days | nginx directly (fastest, 1-year cache) |
| warm | Local disk storage/warm/ | 30 – 90 days | Node.js fallback |
| cold | Hetzner S3 object storage | indefinite | S3 URL redirect |
Archiver runs nightly (02:00 hot→warm, 03:00 warm→cold). Public URLs remain stable across all tiers.