Overview

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.

📄 PDF 📝 Word .docx .doc 📊 Excel .xlsx .xls 📽 PowerPoint .pptx .ppt 📁 ODF .odt .ods .odp 📃 .txt .csv 🖼 JPEG PNG WebP GIF

Authentication

Every POST /api/upload must carry three HTTP headers. Reading (GET) requires no auth.

HeaderValueNotes
X-Upload-Pubkey64-char hexsecp256k1 public key — x-coordinate only (Nostr / LanaCoin style)
X-Upload-TimestampUnix secondsMust be within ±5 minutes of server time
X-Upload-SigHex DER ECDSA signatureSigns the message below

Message being signed (hash input):

"lana-media-upload:" + timestamp_as_string   →  SHA-256  →  sign with ECDSA secp256k1
Generate a fresh timestamp + signature immediately before each request. The ±5 min window prevents replay attacks — never reuse a signature.

Endpoints

POST /api/upload

Upload an image or document. Returns the permanent public URL.

Response — image

{
  "filename":  "3f8a1c2d-…uuid.jpg",
  "url":       "https://media.lanaloves.us/images/3f8a1c2d-….jpg",
  "width":     1024,
  "height":    768,
  "size":      48210,
  "mime_type": "image/jpeg",
  "category":  "image"
}

Response — document

{
  "filename":  "7b3e9f12-…uuid.pdf",
  "url":       "https://media.lanaloves.us/files/7b3e9f12-….pdf",
  "size":      204800,
  "mime_type": "application/pdf",
  "category":  "document"
}

Error codes

StatusMeaning
400Missing headers, invalid pubkey format, unsupported type, or invalid magic bytes
403Invalid signature or timestamp outside ±5 min window
413File too large — images max 10 MB, documents max 50 MB
500Processing or write error (corrupt file)
GET /images/:uuid.jpg

Serve an uploaded image. No auth — fully public. Cached 1 year (Cache-Control: immutable). Served directly by nginx from SSD on hot tier.

GET /files/:uuid.ext

Serve an uploaded document. No auth — fully public. Returns the correct Content-Type for each extension. Cached 1 year.

🔗 URLs are permanent and public. 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.
GET /api/stats

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
}

Node.js Integration

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));

AI / LLM Prompt Snippet

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.
  
🔐 Never log or expose private keys. Build the signature in memory, discard key material after signing. The public key in the header is safe to share — it only identifies the uploader.

Storage Tiers

TierLocationAgeServed 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 storageindefinite S3 URL redirect

Archiver runs nightly (02:00 hot→warm, 03:00 warm→cold). Public URLs remain stable across all tiers.