@kadi.build/file-sharing
v1.2.0
Published
File sharing service with tunneling and local S3-compatible interface
Readme
@kadi.build/file-sharing
File sharing service with tunneling, authentication, and a local S3-compatible interface.
Integrates @kadi.build/file-manager and @kadi.build/tunnel-services into one turnkey solution.
Features
- HTTP File Server — Static file serving with directory listing, range requests, CORS, and multi-scheme authentication
- Local S3-Compatible API — Emulates AWS S3 endpoints locally so you can use
@aws-sdk/client-s3against your local filesystem - Extensible HTTP Pipeline — Register custom middleware and routes (e.g. Docker Registry v2 endpoints) alongside built-in file serving
- Tunnel Integration — Expose your local server publicly via KĀDI (default), ngrok, serveo, localtunnel, or pinggy
- Authentication — Basic Auth, Bearer Token, and API Key (header / query param) for HTTP; AWS SigV4 / SigV2 / Bearer / Basic for S3
- Temporary Credentials — Generate time-limited S3 credentials for secure sharing
- Secrets Management — Automatic
.envloading (walks up parent directories for monorepo support) + environment variables + explicit config - Download Monitoring — Track active downloads, progress, speed, and completion statistics
- Graceful Shutdown — Priority-ordered shutdown with timeout and force-kill
- Webhook Notifications — Event-driven notifications to external endpoints
- Monitoring Dashboard — Console-based real-time dashboard
Installation
npm install @kadi.build/file-sharingQuick Start
Basic File Sharing
import { FileSharingServer } from '@kadi.build/file-sharing';
const server = new FileSharingServer({
staticDir: '/path/to/files',
port: 3000
});
await server.start();
console.log(`Serving files at ${server.getInfo().localUrl}`);
await server.stop();With Public Tunnel (KĀDI)
const server = new FileSharingServer({
staticDir: '/path/to/files',
port: 3000,
tunnel: {
enabled: true
// KĀDI is the default — reads token from KADI_TUNNEL_TOKEN env var or .env
}
});
await server.start();
console.log(`Public URL: ${server.tunnelUrl}`);With Authentication
const server = new FileSharingServer({
staticDir: '/path/to/files',
port: 3000,
auth: { apiKey: 'my-secret-key' }, // or { username: 'admin', password: 'secret' }
tunnel: { enabled: true }
});
await server.start();
// Clients must send X-API-Key header, Bearer token, or ?apiKey= query paramWith Local S3 API
import { FileSharingServer } from '@kadi.build/file-sharing';
import { S3Client, ListObjectsV2Command, PutObjectCommand } from '@aws-sdk/client-s3';
const server = new FileSharingServer({
staticDir: '/path/to/files',
port: 3000,
enableS3: true,
s3Port: 9000
});
await server.start();
// Use the standard AWS SDK against your LOCAL filesystem
const s3 = new S3Client({
endpoint: 'http://localhost:9000',
region: 'us-east-1',
credentials: { accessKeyId: 'minioadmin', secretAccessKey: 'minioadmin' },
forcePathStyle: true
});
const response = await s3.send(new ListObjectsV2Command({ Bucket: 'local' }));
console.log(response.Contents);
await s3.send(new PutObjectCommand({
Bucket: 'local',
Key: 'uploaded.txt',
Body: 'Hello from S3!'
}));Quick Share (One-Liner)
import { createQuickShare } from '@kadi.build/file-sharing';
const { server, localUrl, publicUrl } = await createQuickShare('./my-files', {
tunnel: true,
auth: { apiKey: 'share-key-123' }
});
console.log(`Share this link: ${publicUrl}`);Secrets & Authentication
How Secrets Are Loaded
Secrets are resolved in priority order (first match wins):
- Explicit constructor config — values you pass directly
- Environment variables —
process.env.* .envfile — automatically found by walking up parent directories fromprocess.cwd()
The .env loader supports monorepo layouts: if your .env is at the workspace root and the package runs from packages/file-sharing/, it will find it automatically.
Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| Tunnel — KĀDI | | |
| KADI_TUNNEL_TOKEN | KĀDI authentication token | (required for KĀDI) |
| KADI_TUNNEL_SERVER | KĀDI broker address | broker.kadi.build |
| KADI_TUNNEL_DOMAIN | Tunnel domain | tunnel.kadi.build |
| KADI_TUNNEL_PORT | KĀDI frps server port | 7000 |
| KADI_TUNNEL_SSH_PORT | KĀDI SSH gateway port | 2200 |
| KADI_TUNNEL_MODE | Connection mode: ssh, frpc, or auto | auto |
| KADI_TUNNEL_TRANSPORT | Transport protocol: wss (via gateway on :443) or tcp (direct) | wss |
| KADI_TUNNEL_WSS_HOST | WSS gateway hostname (e.g., tunnel-control.kadi.build) | — |
| KADI_AGENT_ID | Agent identifier for proxy naming | kadi |
| Tunnel — Ngrok | | |
| NGROK_AUTHTOKEN | Ngrok auth token (also accepts NGROK_AUTH_TOKEN) | — |
| HTTP Authentication | | |
| KADI_AUTH_API_KEY | API key for Bearer / X-API-Key auth | — |
| KADI_AUTH_USERNAME | HTTP Basic auth username | — |
| KADI_AUTH_PASSWORD | HTTP Basic auth password | — |
| S3 Authentication | | |
| KADI_S3_ACCESS_KEY | S3 access key ID | minioadmin |
| KADI_S3_SECRET_KEY | S3 secret access key | minioadmin |
.env File Example
# Tunnel credentials
KADI_TUNNEL_TOKEN=your-kadi-token-here
KADI_TUNNEL_SERVER=broker.kadi.build
KADI_TUNNEL_DOMAIN=tunnel.kadi.build
KADI_TUNNEL_PORT=7000
KADI_TUNNEL_SSH_PORT=2200
KADI_TUNNEL_MODE=ssh
KADI_TUNNEL_TRANSPORT=wss
KADI_TUNNEL_WSS_HOST=tunnel-control.kadi.build
KADI_AGENT_ID=my-agent
# Optional: Ngrok (fallback)
NGROK_AUTHTOKEN=your-ngrok-token
# HTTP auth (optional — protects file downloads)
KADI_AUTH_API_KEY=my-secret-api-key
# S3 credentials (optional — default minioadmin/minioadmin)
KADI_S3_ACCESS_KEY=my-access-key
KADI_S3_SECRET_KEY=my-secret-keyHTTP Authentication Schemes
When auth is configured (via config, env vars, or .env), the HTTP server supports three authentication schemes:
| Scheme | How to Authenticate |
|--------|---------------------|
| Basic Auth | Authorization: Basic <base64(user:pass)> |
| Bearer Token | Authorization: Bearer <apiKey> |
| API Key | X-API-Key: <apiKey> header or ?apiKey=<key> query param |
If auth.apiKey is set, all three token-based methods are accepted.
If auth.username + auth.password are set, only Basic Auth is accepted.
S3 Authentication
When custom S3 credentials are set (anything other than the default minioadmin/minioadmin), the S3 server validates requests:
| Method | How It Works |
|--------|-------------|
| AWS SigV4 | Standard Authorization: AWS4-HMAC-SHA256 Credential=<accessKeyId>/... |
| AWS SigV2 | Legacy Authorization: AWS <accessKeyId>:signature |
| Bearer | Convenience Authorization: Bearer <accessKeyId> |
| Basic | Docker-style Authorization: Basic base64(accessKeyId:secretAccessKey) |
| Pre-signed URL | ?X-Amz-Credential=<accessKeyId>/... query param |
With default credentials (
minioadmin/minioadmin), auth is skipped for backward compatibility unless you setenforceAuth: truein the S3 config.
API Reference
FileSharingServer
Main orchestrating class.
const server = new FileSharingServer({
// File serving
staticDir: process.cwd(), // Directory to serve
port: 3000, // HTTP port
host: '0.0.0.0', // Bind address
enableDirectoryListing: true, // Show directory listing
cors: true, // Enable CORS
// Authentication (or use env vars / .env)
auth: null, // { apiKey: 'key' } or { username: 'u', password: 'p' }
// S3 API
enableS3: false, // Enable S3-compatible API
s3Port: 9000, // S3 API port
s3Config: { // Passed to S3Server
accessKeyId: 'minioadmin',
secretAccessKey: 'minioadmin',
bucketName: 'local',
region: 'us-east-1',
enforceAuth: false // Force auth even with default creds
},
// Tunnel
tunnel: {
enabled: false, // Start tunnel on server.start()
service: 'kadi', // 'kadi' | 'ngrok' | 'serveo' | 'localtunnel' | 'pinggy'
autoFallback: true, // Fall back to other services on failure
autoReconnect: true, // Auto-reconnect on tunnel drop
reconnectDelay: 5000, // ms between reconnect attempts
// KĀDI-specific (or use env vars / .env)
kadiToken: undefined,
kadiServer: undefined,
kadiDomain: undefined,
kadiPort: undefined, // frps server port (NOT the local port)
kadiSshPort: undefined,
kadiMode: undefined, // 'ssh' | 'frpc' | 'auto'
kadiAgentId: undefined,
kadiTransport: undefined, // 'wss' (default) | 'tcp'
kadiWssControlHost: undefined, // WSS gateway hostname
// Ngrok-specific
ngrokAuthToken: undefined,
// Advanced
managerOptions: {} // Extra options passed to TunnelManager
},
// Shutdown
shutdown: {
gracefulTimeout: 30000,
finishActiveDownloads: true,
forceKillTimeout: 60000
},
// Monitoring
monitoring: {
enabled: true,
dashboard: false, // Enable console dashboard
webhooks: [] // ['https://hooks.example.com/events']
}
});Methods
| Method | Returns | Description |
|--------|---------|-------------|
| start() | Promise<ServerInfo> | Start HTTP server (+ S3 and tunnel if enabled) |
| stop() | Promise<void> | Gracefully stop all servers and tunnels |
| enableTunnel(options?) | Promise<TunnelInfo> | Enable public tunnel after start |
| disableTunnel() | Promise<void> | Close active tunnel |
| enableS3(options?) | Promise<{endpoint, port}> | Enable S3 API dynamically |
| disableS3() | Promise<void> | Disable S3 API |
| getInfo() | ServerInfo | Get server info, URLs, and tunnel status |
| getStats() | DownloadStats | Get download statistics |
| listFiles(subPath?) | Promise<FileEntry[]> | List files in served directory |
| addWebhook(url, events?) | void | Register a webhook endpoint |
| removeWebhook(url) | void | Remove a webhook |
Properties
| Property | Type | Description |
|----------|------|-------------|
| isRunning | boolean | Whether the server is running |
| port | number | Current HTTP port |
| tunnelUrl | string \| null | Current public tunnel URL |
| staticDir | string | Directory being served |
| tunnel | object \| null | Active tunnel info |
| tunnelManager | TunnelManager | Underlying tunnel manager |
Events
| Event | Payload | When |
|-------|---------|------|
| started | ServerInfo | Server fully started |
| stopping | — | Shutdown initiated |
| stopped | — | Server fully stopped |
| download:start | {id, file, ...} | File download begins |
| download:complete | {id, file, duration, ...} | Download finished |
| download:error | {id, error} | Download failed |
| upload:complete | {file, size} | S3 upload finished |
| s3:started | {port, endpoint} | S3 server started |
| s3:get | {bucket, key} | S3 GetObject |
| s3:put | {bucket, key} | S3 PutObject |
| s3:delete | {bucket, key} | S3 DeleteObject |
| bucket:removed | {bucket} | Bucket programmatically removed |
| credentials:generated | {accessKey, expiresAt} | Temporary credentials created |
| middleware:added | {name, priority} | HTTP middleware registered |
| middleware:removed | {name} | HTTP middleware removed |
| route:added | {method, path, priority} | Custom HTTP route registered |
| route:removed | {method, path} | Custom HTTP route removed |
| tunnel:created | TunnelInfo | Tunnel established |
| tunnel:closed | — | Tunnel shut down |
| tunnel:error | Error | Tunnel failed (non-fatal) |
| http:started | {port, host} | HTTP server listening |
| http:error | Error | HTTP error |
HttpServerProvider
Low-level HTTP file server with authentication and extensibility.
import { HttpServerProvider } from '@kadi.build/file-sharing/http';
const httpServer = new HttpServerProvider({
port: 3000,
staticDir: './files',
enableDirectoryListing: true,
cors: true,
auth: { apiKey: 'my-key' } // or { username: 'u', password: 'p' }
});
await httpServer.start();Middleware (Extensibility)
Register middleware that runs before built-in auth and static file handling. Higher priority runs first.
// Add authentication middleware (priority 10 = runs before default handlers)
httpServer.addMiddleware('dockerAuth', (req, res, next) => {
if (req.url.startsWith('/v2')) {
const auth = req.headers.authorization;
if (!auth || !isValidToken(auth)) {
res.writeHead(401);
res.end(JSON.stringify({ errors: [{ code: 'UNAUTHORIZED' }] }));
return;
}
req.user = { token: auth };
}
next();
}, { priority: 10 });
// List registered middleware
httpServer.getMiddleware(); // [{ name: 'dockerAuth', priority: 10 }]
// Remove middleware
httpServer.removeMiddleware('dockerAuth');Custom Routes (Extensibility)
Register custom route handlers with Express-style :param path patterns. Custom routes match before built-in static file serving.
// Docker Registry v2 ping
httpServer.addCustomRoute('GET', '/v2/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{}');
});
// Route with path parameters — req.params is populated automatically
httpServer.addCustomRoute('GET', '/v2/:name/manifests/:reference', (req, res) => {
console.log(req.params); // { name: 'myapp', reference: 'latest' }
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(getManifest(req.params.name, req.params.reference)));
});
// HEAD method support
httpServer.addCustomRoute('HEAD', '/v2/:name/blobs/:digest', (req, res) => {
res.writeHead(200, { 'Docker-Content-Digest': req.params.digest });
res.end();
});
// List / remove routes
httpServer.getCustomRoutes(); // [{ method, path, priority }]
httpServer.removeCustomRoute('GET', '/v2/'); // true if foundRequest Pipeline Order
Request → CORS → Middleware Chain → Custom Routes → Built-in Auth → Static FilesS3Server
Local S3-compatible API server. Files are stored on disk, not on AWS.
import { S3Server } from '@kadi.build/file-sharing/s3';
const s3 = new S3Server({
port: 9000,
rootDir: './data',
bucketName: 'local',
region: 'us-east-1',
accessKeyId: 'my-key', // default: 'minioadmin'
secretAccessKey: 'my-secret', // default: 'minioadmin'
enforceAuth: true // validate even with default creds
});
const info = await s3.start();
console.log(info.serverId); // 'kadi-s3-9000'Supported S3 Operations:
ListBuckets,CreateBucket,DeleteBucket,HeadBucketListObjects,ListObjectsV2GetObject(with range requests)PutObjectDeleteObjectHeadObjectCreateMultipartUpload,UploadPart,CompleteMultipartUpload
Programmatic Authentication Validation
Validate incoming requests against configured credentials. Returns a structured result — useful for layering custom auth (e.g. Docker Registry) on top of S3.
const result = s3.validateAuthentication(req);
// Success: { success: true, user: { accessKey: 'AKIA...' } }
// Failure: { success: false, error: 'Invalid credentials' }Supports AWS SigV4, SigV2, Bearer, Basic Auth (Docker clients), and pre-signed URL query params.
Temporary Credentials
Generate time-limited credentials for secure sharing:
const creds = s3.generateTemporaryCredentials({ ttl: 3600 }); // 1 hour
console.log(creds.accessKey); // 'AKIA...' (random)
console.log(creds.secretKey); // random hex string
console.log(creds.expiresAt); // '2026-02-16T...' (ISO string)
console.log(creds.expiry); // Date object (backward compat)
// Temp credentials are automatically validated by _checkS3Auth and validateAuthenticationProgrammatic Bucket Removal
Force-delete a bucket directory and all its contents (for cleanup):
await s3.removeBucket('container-id-123');
// Emits 'bucket:removed' event. Idempotent (no error if bucket doesn't exist).Properties
| Property | Type | Description |
|----------|------|-------------|
| serverId | string | Stable server identifier (e.g. kadi-s3-9000) |
| isRunning | boolean | Whether the server is running |
createQuickShare
One-liner to share a directory with optional tunnel and auth.
import { createQuickShare } from '@kadi.build/file-sharing';
const { server, localUrl, publicUrl } = await createQuickShare('./my-files', {
port: 3000, // HTTP port (default: 3000)
tunnel: true, // Enable tunnel (default: false)
tunnelService: 'kadi', // Tunnel service (default: 'kadi')
kadiToken: 'token', // Or set KADI_TUNNEL_TOKEN env
ngrokAuthToken: 'token', // Or set NGROK_AUTHTOKEN env
auth: { apiKey: 'share-key' }, // Protect downloads
tunnelOptions: {} // Extra TunnelManager options
});
console.log(`Local: ${localUrl}`);
console.log(`Public: ${publicUrl}`);DownloadMonitor
Track download progress and statistics.
import { DownloadMonitor } from '@kadi.build/file-sharing';
const monitor = new DownloadMonitor();
monitor.on('download:start', (dl) => console.log(`Started: ${dl.file}`));
monitor.on('download:progress', (dl) => console.log(`${dl.progress.toFixed(1)}%`));
monitor.on('download:complete', (dl) => console.log(`Done in ${dl.duration}ms`));
monitor.startDownload('id', { file: 'test.txt', totalSize: 1000 });
monitor.updateProgress('id', 500);
monitor.completeDownload('id');
console.log(monitor.getStats());
// { activeCount, completedCount, totalBytes, peakConcurrent, ... }ShutdownManager
Graceful shutdown with priority-ordered callbacks.
import { ShutdownManager } from '@kadi.build/file-sharing';
const sm = new ShutdownManager({ gracefulTimeout: 10000 });
sm.register(async () => console.log('Close DB'), 1); // priority 1 = first
sm.register(async () => console.log('Close HTTP'), 10); // priority 10 = later
await sm.shutdown();Sub-path Exports
// Main export — all classes and utilities
import { FileSharingServer, createQuickShare } from '@kadi.build/file-sharing';
// S3 server only
import { S3Server } from '@kadi.build/file-sharing/s3';
// HTTP server only
import { HttpServerProvider } from '@kadi.build/file-sharing/http';
// Re-exports from dependencies (convenience)
import { FileManager, createFileManager, TunnelManager } from '@kadi.build/file-sharing';Tunnel Services
KĀDI is the default and recommended tunnel service. It supports two connection modes:
| Mode | How It Works | Requirements |
|------|-------------|--------------|
| SSH (default) | ssh -R through KĀDI's SSH gateway — zero-dependency | SSH client in $PATH |
| frpc | Uses the frp client binary for higher reliability | frpc binary in $PATH |
| auto | Prefers frpc if available, falls back to SSH | — |
Set KADI_TUNNEL_MODE=ssh (or frpc) in your .env to force a specific mode.
Transport Protocol
When using frpc mode, the control channel transport can be configured:
| Transport | Description | Default | |-----------|-------------|---------| | wss | Routes the frpc control channel through a WSS gateway on port 443. Works reliably on enterprise/campus networks that block non-standard ports. | ✅ Default | | tcp | Direct TCP connection to the frps server port (typically 7000). | — |
Set KADI_TUNNEL_TRANSPORT=wss and KADI_TUNNEL_WSS_HOST=tunnel-control.kadi.build in your .env to use WSS transport. TCP mode can be forced with KADI_TUNNEL_TRANSPORT=tcp.
If KĀDI credentials are not provided, the tunnel will automatically fall back to free services (serveo → localtunnel → pinggy → localhost.run) unless autoFallback: false is set.
Dependencies
| Package | Purpose |
|---------|---------|
| @kadi.build/file-manager | File management utilities |
| @kadi.build/tunnel-services | Multi-provider tunnel management |
| express | HTTP framework |
| cors | CORS middleware |
| mime-types | MIME type detection |
| xml2js | XML generation for S3 responses |
| chalk | Console styling |
Requirements
- Node.js ≥ 18.0.0
License
MIT
