@betechspark/fileserver-server
v0.1.1
Published
Server-side helpers for the betechspark File Server — HMAC-signed download/upload URLs and admin operations.
Maintainers
Readme
@betechspark/fileserver-server
Server-side helpers for the betechspark File Server — HMAC-signed download/upload URLs and admin operations. Built on @betechspark/fileserver-client.
npm: @betechspark/fileserver-server · Related: fileserver-client · fileserver-react
Node.js only — uses
node:crypto. Never import in browser bundles or exposehmacSecretto the client.
Installation
npm install @betechspark/fileserver-server @betechspark/fileserver-clientFILESERVER_SIGNING_SECRET must match the File Server configuration.
Quick Start
import { createFileserverServer } from '@betechspark/fileserver-server';
const secret = process.env.FILESERVER_SIGNING_SECRET;
const baseUrl =
process.env.FILESERVER_INTERNAL_URL ??
process.env.NEXT_PUBLIC_FILESERVER_URL ??
'http://127.0.0.1:3004';
if (!secret) throw new Error('FILESERVER_SIGNING_SECRET is required');
const fs = createFileserverServer({ baseUrl, hmacSecret: secret });
// Signed download URL (default TTL: 300 seconds)
const signed = fs.signDownloadUrl(fileId, 300);
// → { url, expUnix, signature }
return Response.json({ url: signed.url, expUnix: signed.expUnix });Examples
Next.js — signed download Route Handler
// app/api/sign-download/route.ts
import { createFileserverServer } from '@betechspark/fileserver-server';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const secret = process.env.FILESERVER_SIGNING_SECRET;
const baseUrl = process.env.FILESERVER_INTERNAL_URL ?? 'http://127.0.0.1:3004';
if (!secret) {
return NextResponse.json({ error: 'Signing not configured' }, { status: 501 });
}
const fileId = new URL(request.url).searchParams.get('fileId');
if (!fileId) {
return NextResponse.json({ error: 'Missing fileId' }, { status: 400 });
}
// TODO: authorize — only sign files the current user may access
const fs = createFileserverServer({ baseUrl, hmacSecret: secret });
const signed = fs.signDownloadUrl(fileId, 300);
return NextResponse.json({ url: signed.url, expUnix: signed.expUnix });
}Browser usage:
const res = await fetch(`/api/sign-download?fileId=${encodeURIComponent(fileId)}`);
const { url } = await res.json();
// <img src={url} alt="" />Next.js — server-side upload + sign in one flow
// app/api/avatar/route.ts
import { createFileserverServer } from '@betechspark/fileserver-server';
import { uploadFile } from '@betechspark/fileserver-client';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const secret = process.env.FILESERVER_SIGNING_SECRET!;
const baseUrl = process.env.FILESERVER_INTERNAL_URL ?? 'http://127.0.0.1:3004';
const fs = createFileserverServer({ baseUrl, hmacSecret: secret });
const form = await request.formData();
const file = form.get('file');
const userId = form.get('userId');
if (!(file instanceof File) || typeof userId !== 'string') {
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
}
const uploaded = await uploadFile(fs.gateway, {
file,
filename: file.name,
ownerId: `user:${userId}`,
visibility: 'private',
});
const signed = fs.signDownloadUrl(uploaded.id, 600);
return NextResponse.json({
fileId: uploaded.id,
previewUrl: signed.url,
expiresAt: signed.expUnix,
});
}Presigned upload URL
Give clients a time-limited URL to POST directly to the File Server:
const fs = createFileserverServer({ baseUrl, hmacSecret: secret });
const presigned = fs.signUploadUrl('user:abc', 900);
// presigned.url → POST multipart here within 15 minutes
// Client still sends: file, owner_id (must match signed owner)Server-side metadata + admin GC
const fs = createFileserverServer({ baseUrl, hmacSecret: secret });
// Same gateway as fileserver-client — use from Node/cron jobs
const meta = await fs.gateway.getMeta(fileId);
console.log(meta.filename, meta.sizeBytes);
// Trigger blob garbage collection (ops/maintenance)
await fs.admin.triggerBlobGc();Hono / Fastify — reusable signer factory
import { createFileserverServer } from '@betechspark/fileserver-server';
function createSigner() {
const secret = process.env.FILESERVER_SIGNING_SECRET;
const baseUrl = process.env.FILESERVER_INTERNAL_URL ?? 'http://127.0.0.1:3004';
if (!secret) throw new Error('FILESERVER_SIGNING_SECRET required');
return createFileserverServer({ baseUrl, hmacSecret: secret });
}
// Hono
app.get('/files/:id/url', (c) => {
const fs = createSigner();
const signed = fs.signDownloadUrl(c.req.param('id'), 300);
return c.json({ url: signed.url });
});Security checklist
| Do | Don't |
|----|-------|
| Keep FILESERVER_SIGNING_SECRET in server env only | Ship secret in NEXT_PUBLIC_* or client bundles |
| Check ownership before signing | Sign any fileId from query params without auth |
| Use short TTL (300–900 s) for download URLs | Use multi-day signed URLs for user content |
| Use FILESERVER_INTERNAL_URL inside Docker/K8s | Call public URL from backend when internal exists |
When to use
| Use on server | Do not use in browser |
|---------------|------------------------|
| Next.js Route Handlers, Server Actions, Node APIs | Never expose hmacSecret to the client |
The hmacSecret must match the File Server’s FILESERVER_SIGNING_SECRET. Signing uses HMAC-SHA256 base64url, same as the Rust service.
createFileserverServer API
createFileserverServer(options: { baseUrl: string; hmacSecret: string })Returns:
| Member | Description |
|--------|-------------|
| signDownloadUrl(fileId, expiresInSeconds?) | GET /files/:id?exp=…&sig=…&perm=view |
| signUploadUrl(ownerId, expiresInSeconds?) | Signed POST /files query URL |
| gateway | Same FileserverGateway as @betechspark/fileserver-client |
| admin.triggerBlobGc() | POST /admin/gc |
Signed URL shape
Download: {baseUrl}/files/{fileId}?exp={expUnix}&sig={signature}&perm=view
Upload: {baseUrl}/files?owner_id={ownerId}&exp={expUnix}&sig={signature}&perm=view
Environment variables
| Variable | Purpose |
|----------|---------|
| FILESERVER_SIGNING_SECRET | Shared HMAC secret (must match File Server) |
| FILESERVER_INTERNAL_URL | Internal URL (Docker/K8s service name) |
| NEXT_PUBLIC_FILESERVER_URL | Fallback public URL (default http://127.0.0.1:3004) |
Exports
createFileserverServerSignedUrl,SignOptions(domain types)
Import HTTP helpers from @betechspark/fileserver-client when you do not need signing.
Detailed usage (step-by-step)
See USAGE.md in the monorepo for a numbered walkthrough.
Monorepo development
pnpm turbo run build --filter=@betechspark/fileserver-client --filter=@betechspark/fileserver-serverRelated
@betechspark/fileserver-client— HTTP gateway and use cases@betechspark/fileserver-react— React hooks (use server signing for protected assets)- betechspark File Server — Rust service (port 3004)
