@julesl23/s5js
Version:
Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities
287 lines • 11.2 kB
JavaScript
import express from 'express';
import { WebSocket } from 'ws';
import { S5Node } from './node/node.js';
import { S5UserIdentity } from './identity/identity.js';
import { S5APIWithIdentity } from './identity/api.js';
import { JSCryptoImplementation } from './api/crypto/js.js';
import { MemoryLevelStore } from './kv/memory_level.js';
import { BlobIdentifier } from './identifier/blob.js';
// Polyfill WebSocket for Node.js
globalThis.WebSocket = WebSocket;
const app = express();
const PORT = process.env.PORT || 5522;
const S5_SEED_PHRASE = process.env.S5_SEED_PHRASE;
let s5Api;
let userIdentity;
// Simple in-memory storage for demo purposes
// In production, use a proper database or file storage
const localBlobStorage = new Map();
// Add in-memory storage for vector-db compatibility
const storage = new Map();
// Middleware to parse both JSON and raw binary data
app.use(express.json()); // Parse JSON bodies
app.use(express.raw({ type: '*/*', limit: '100mb' })); // Parse raw binary for other content types
// Initialize S5 client with Node.js-compatible storage
async function initializeS5() {
try {
// Create crypto implementation
const crypto = new JSCryptoImplementation();
// Create S5 node with memory storage (Node.js compatible)
const node = new S5Node(crypto);
// Initialize with memory-level store instead of IndexedDB
await node.init(async (name) => {
return await MemoryLevelStore.open();
});
// Connect to default peers with error handling
const defaultPeers = [
'wss://z2Das8aEF7oNoxkcrfvzerZ1iBPWfm6D7gy3hVE4ALGSpVB@node.sfive.net/s5/p2p',
'wss://z2DdbxV4xyoqWck5pXXJdVzRnwQC6Gbv6o7xDvyZvzKUfuj@s5.vup.dev/s5/p2p',
'wss://z2DWuWNZcdSyZLpXFK2uCU3haaWMXrDAgxzv17sDEMHstZb@s5.garden/s5/p2p',
];
// Try to connect to peers but don't fail if connections fail
// We'll wrap the connections to handle errors gracefully
let connectedPeers = 0;
for (const uri of defaultPeers) {
try {
// The connectToNode method doesn't throw immediately, but we can add error handling
// to the WebSocket after it's created
const peerName = uri.split('@')[1];
console.log(`Attempting to connect to peer: ${peerName}`);
// Connect to the node
node.p2p.connectToNode(uri);
// Get the peer and add error handling
const peer = node.p2p.peers.get(uri);
if (peer && peer.socket) {
peer.socket.onerror = (error) => {
console.warn(`WebSocket error for ${peerName}:`, error);
};
peer.socket.onclose = () => {
console.log(`Disconnected from ${peerName}`);
};
// Track successful connections
peer.socket.onopen = () => {
connectedPeers++;
console.log(`Connected to ${peerName}`);
};
}
}
catch (error) {
console.warn(`Failed to initiate connection to peer:`, error instanceof Error ? error.message : 'Unknown error');
}
}
// Don't wait for network initialization if connections fail
// The server can still work for local operations
try {
// Wait briefly for connections with a timeout
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Network initialization timeout')), 5000));
await Promise.race([node.ensureInitialized(), timeout]);
console.log('Successfully connected to S5 network');
}
catch (error) {
console.warn('Could not connect to S5 network, continuing in offline mode');
console.warn('Note: Upload/download operations may be limited');
}
// Set up API with or without identity
if (S5_SEED_PHRASE) {
// Create user identity from seed phrase
userIdentity = await S5UserIdentity.fromSeedPhrase(S5_SEED_PHRASE, crypto);
// Create auth store
const authStore = await MemoryLevelStore.open();
// Create API with identity
const apiWithIdentity = new S5APIWithIdentity(node, userIdentity, authStore);
await apiWithIdentity.initStorageServices();
s5Api = apiWithIdentity;
console.log('User identity initialized from seed phrase');
}
else {
// Use node directly as API
s5Api = node;
}
console.log(`S5 client initialized and connected to network`);
return true;
}
catch (error) {
console.error('Failed to initialize S5 client:', error);
return false;
}
}
// Health check endpoint
app.get('/api/v1/health', async (req, res) => {
try {
const health = {
status: 'healthy',
s5: {
connected: !!s5Api,
authenticated: !!userIdentity
},
timestamp: new Date().toISOString()
};
res.json(health);
}
catch (error) {
res.status(500).json({
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Upload endpoint
app.post('/api/v1/upload', async (req, res) => {
try {
if (!s5Api) {
return res.status(503).json({ error: 'S5 API not initialized' });
}
const data = req.body;
if (!data || data.length === 0) {
return res.status(400).json({ error: 'No data provided' });
}
// Check if we have authentication (required for actual S5 uploads)
if (!userIdentity) {
// Without authentication, we can only store locally and generate a CID
// This is a simplified implementation for testing
const crypto = s5Api.crypto;
// Ensure data is a Uint8Array
const dataArray = new Uint8Array(data);
const hash = crypto.hashBlake3Sync(dataArray);
const blobId = new BlobIdentifier(new Uint8Array([0x1f, ...hash]), // MULTIHASH_BLAKE3 prefix
dataArray.length);
// Store locally in memory
const cidString = blobId.toString();
localBlobStorage.set(cidString, data);
console.log(`Stored blob locally with CID: ${cidString}`);
res.json({
cid: cidString,
size: data.length,
timestamp: new Date().toISOString(),
note: 'Stored locally (no S5 authentication)'
});
}
else {
// With authentication, upload to S5 network
const blob = new Blob([data]);
const blobId = await s5Api.uploadBlob(blob);
res.json({
cid: blobId.toString(),
size: data.length,
timestamp: new Date().toISOString()
});
}
}
catch (error) {
console.error('Upload error:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Upload failed'
});
}
});
// Download endpoint
app.get('/api/v1/download/:cid', async (req, res) => {
try {
if (!s5Api) {
return res.status(503).json({ error: 'S5 API not initialized' });
}
const { cid } = req.params;
if (!cid) {
return res.status(400).json({ error: 'CID parameter required' });
}
// First check local storage
if (localBlobStorage.has(cid)) {
const data = localBlobStorage.get(cid);
console.log(`Serving blob from local storage: ${cid}`);
res.set('Content-Type', 'application/octet-stream');
res.set('X-CID', cid);
res.set('X-Source', 'local');
res.send(data);
return;
}
// If not in local storage, try to download from S5 network
try {
const blobId = BlobIdentifier.decode(cid);
const data = await s5Api.downloadBlobAsBytes(blobId.hash);
if (!data) {
return res.status(404).json({ error: 'Content not found' });
}
// Set appropriate headers and send binary data
res.set('Content-Type', 'application/octet-stream');
res.set('X-CID', cid);
res.set('X-Source', 's5-network');
res.send(Buffer.from(data));
}
catch (downloadError) {
// If download fails, return not found
console.error('Download from S5 failed:', downloadError);
res.status(404).json({ error: 'Content not found in local storage or S5 network' });
}
}
catch (error) {
console.error('Download error:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Download failed'
});
}
});
// Storage endpoints for vector-db
app.put('/s5/fs/:type/:id', (req, res) => {
const { type, id } = req.params;
const key = `${type}/${id}`;
storage.set(key, req.body);
console.log(`Stored ${key}`);
res.json({ success: true, key });
});
app.get('/s5/fs/:type/:id', (req, res) => {
const { type, id } = req.params;
const key = `${type}/${id}`;
const data = storage.get(key);
if (data) {
res.json(data);
}
else {
res.status(404).json({ error: 'Not found' });
}
});
app.delete('/s5/fs/:type/:id', (req, res) => {
const { type, id } = req.params;
const key = `${type}/${id}`;
const deleted = storage.delete(key);
res.json({ success: deleted });
});
// List endpoint
app.get('/s5/fs/:type', (req, res) => {
const { type } = req.params;
const items = Array.from(storage.keys())
.filter(key => key.startsWith(`${type}/`))
.map(key => key.split('/')[1]);
res.json({ items });
});
// Start server
async function startServer() {
const initialized = await initializeS5();
if (!initialized) {
console.error('Failed to initialize S5 client. Server will run with limited functionality.');
}
app.listen(PORT, () => {
console.log(`S5 Server running on port ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/api/v1/health`);
if (S5_SEED_PHRASE) {
console.log('Authentication: Enabled (seed phrase provided)');
}
else {
console.log('Authentication: Disabled (no seed phrase provided)');
}
});
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down S5 server...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\nShutting down S5 server...');
process.exit(0);
});
// Start the server
startServer().catch(error => {
console.error('Failed to start server:', error);
process.exit(1);
});
//# sourceMappingURL=server.js.map