#!/usr/bin/env node
/**
* Oceanir Memory API Server
*
* REST API for persistent AI memory.
*
* Tiers:
* - Free: 1000 memories, 100 req/day
* - Pro ($9/mo): Unlimited memories, 10k req/day
* - Enterprise: Self-hosted, unlimited
*/
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import jwt from 'jsonwebtoken';
import { getStore, Entity } from './store.js';
const app = express();
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'oceanir-memory-dev-secret';
const store = getStore();
// Tiers
const TIERS = {
free: { maxMemories: 1000, dailyRequests: 100 },
pro: { maxMemories: -1, dailyRequests: 10000 },
enterprise: { maxMemories: -1, dailyRequests: -1 },
};
// Rate limiting (simple in-memory)
const requestCounts = new Map<string, { count: number; resetAt: number }>();
app.use(helmet());
app.use(cors());
app.use(express.json());
// Auth middleware
interface AuthRequest extends express.Request {
user?: { id: string; tier: keyof typeof TIERS };
}
const authenticate = (req: AuthRequest, res: express.Response, next: express.NextFunction) => {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) {
req.user = { id: req.ip || 'anon', tier: 'free' };
return next();
}
try {
const decoded = jwt.verify(auth.split(' ')[1], JWT_SECRET) as any;
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
const rateLimit = (req: AuthRequest, res: express.Response, next: express.NextFunction) => {
const userId = req.user?.id || 'anon';
const tier = TIERS[req.user?.tier || 'free'];
const now = Date.now();
let record = requestCounts.get(userId);
if (!record || record.resetAt < now) {
record = { count: 0, resetAt: now + 86400000 };
}
record.count++;
requestCounts.set(userId, record);
if (tier.dailyRequests > 0 && record.count > tier.dailyRequests) {
return res.status(429).json({ error: 'Rate limit exceeded', upgrade: 'https://oceanir.ai/pricing' });
}
next();
};
// Routes
app.get('/', (req, res) => {
res.json({
name: 'Oceanir Memory API',
version: '1.0.0',
docs: 'https://oceanir.ai/docs/memory',
endpoints: {
'POST /remember': 'Store a memory',
'GET /recall': 'Search memories',
'POST /observe/:id': 'Add observation',
'POST /relate': 'Create relation',
'GET /entity/:id': 'Get entity',
'DELETE /entity/:id': 'Delete entity',
'GET /preferences': 'Get preferences',
'GET /patterns': 'Get patterns',
'GET /stats': 'Memory stats',
},
});
});
// Remember
app.post('/remember', authenticate, rateLimit, (req: AuthRequest, res) => {
const { name, type, content, metadata } = req.body;
if (!name || !type || !content) {
return res.status(400).json({ error: 'name, type, content required' });
}
const validTypes: Entity['type'][] = ['person', 'project', 'file', 'concept', 'preference', 'pattern', 'error', 'solution'];
if (!validTypes.includes(type)) {
return res.status(400).json({ error: `Invalid type. Must be: ${validTypes.join(', ')}` });
}
const entity = store.createEntity({ name, type, content, metadata });
res.json({ success: true, entity });
});
// Recall (search)
app.get('/recall', authenticate, rateLimit, (req: AuthRequest, res) => {
const query = req.query.q as string;
const limit = parseInt(req.query.limit as string) || 10;
if (!query) {
return res.status(400).json({ error: 'Query parameter q required' });
}
const results = store.search(query, limit);
res.json({ query, count: results.length, results });
});
// Add observation
app.post('/observe/:id', authenticate, rateLimit, (req: AuthRequest, res) => {
const { id } = req.params;
const { content, source = 'api' } = req.body;
if (!content) {
return res.status(400).json({ error: 'content required' });
}
const observation = store.addObservation(id, content, source);
res.json({ success: true, observation });
});
// Create relation
app.post('/relate', authenticate, rateLimit, (req: AuthRequest, res) => {
const { fromId, toId, type, strength = 0.5 } = req.body;
if (!fromId || !toId || !type) {
return res.status(400).json({ error: 'fromId, toId, type required' });
}
const relation = store.createRelation(fromId, toId, type, strength);
res.json({ success: true, relation });
});
// Get entity
app.get('/entity/:id', authenticate, rateLimit, (req: AuthRequest, res) => {
const entity = store.getEntity(req.params.id);
if (!entity) {
return res.status(404).json({ error: 'Entity not found' });
}
const observations = store.getObservations(entity.id);
const relations = store.getRelations(entity.id);
res.json({ entity, observations, relations });
});
// Delete entity
app.delete('/entity/:id', authenticate, rateLimit, (req: AuthRequest, res) => {
const deleted = store.deleteEntity(req.params.id);
res.json({ success: deleted });
});
// Get preferences
app.get('/preferences', authenticate, rateLimit, (req: AuthRequest, res) => {
const preferences = store.getByType('preference');
res.json({ count: preferences.length, preferences });
});
// Get patterns
app.get('/patterns', authenticate, rateLimit, (req: AuthRequest, res) => {
const patterns = store.getByType('pattern');
const solutions = store.getByType('solution');
res.json({ patterns, solutions });
});
// Get graph
app.get('/graph/:id', authenticate, rateLimit, (req: AuthRequest, res) => {
const depth = parseInt(req.query.depth as string) || 2;
const graph = store.getGraph(req.params.id, depth);
res.json(graph);
});
// Stats
app.get('/stats', authenticate, rateLimit, (req: AuthRequest, res) => {
const stats = store.stats();
res.json(stats);
});
// Start
app.listen(PORT, () => {
console.log(`
╔═══════════════════════════════════════════╗
║ Oceanir Memory API v1.0.0 ║
╠═══════════════════════════════════════════╣
║ http://localhost:${PORT} ║
║ ║
║ POST /remember - Store memory ║
║ GET /recall?q= - Search memories ║
║ POST /observe/:id - Add observation ║
║ POST /relate - Create relation ║
║ GET /entity/:id - Get entity ║
║ GET /stats - Memory stats ║
╚═══════════════════════════════════════════╝
`);
});
export { app, TIERS };