mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-09 18:33:24 +00:00
Compare commits
6 Commits
67b1decbad
...
3efed6e9d2
| Author | SHA1 | Date | |
|---|---|---|---|
| 3efed6e9d2 | |||
| 1257867dff | |||
| 52cf734858 | |||
| 5622867411 | |||
| ac0e064e34 | |||
| e7cd90b905 |
48
src/app.ts
48
src/app.ts
@@ -61,7 +61,7 @@ export function createApp(storage: Storage, config: Config) {
|
||||
|
||||
/**
|
||||
* POST /register
|
||||
* Register a new peer (still needed for peer ID generation)
|
||||
* Register a new peer
|
||||
*/
|
||||
app.post('/register', async (c) => {
|
||||
try {
|
||||
@@ -182,9 +182,14 @@ export function createApp(storage: Storage, config: Config) {
|
||||
* Publish a service
|
||||
*/
|
||||
app.post('/services', authMiddleware, async (c) => {
|
||||
let username: string | undefined;
|
||||
let serviceFqn: string | undefined;
|
||||
let offers: any[] = [];
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { username, serviceFqn, sdp, ttl, isPublic, metadata, signature, message } = body;
|
||||
({ username, serviceFqn } = body);
|
||||
const { sdp, ttl, isPublic, metadata, signature, message } = body;
|
||||
|
||||
if (!username || !serviceFqn || !sdp) {
|
||||
return c.json({ error: 'Missing required parameters: username, serviceFqn, sdp' }, 400);
|
||||
@@ -212,6 +217,15 @@ export function createApp(storage: Storage, config: Config) {
|
||||
return c.json({ error: 'Invalid signature for username' }, 403);
|
||||
}
|
||||
|
||||
// Delete existing service if one exists (upsert behavior)
|
||||
const existingUuid = await storage.queryService(username, serviceFqn);
|
||||
if (existingUuid) {
|
||||
const existingService = await storage.getServiceByUuid(existingUuid);
|
||||
if (existingService) {
|
||||
await storage.deleteService(existingService.id, username);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate SDP
|
||||
if (typeof sdp !== 'string' || sdp.length === 0) {
|
||||
return c.json({ error: 'Invalid SDP' }, 400);
|
||||
@@ -230,7 +244,7 @@ export function createApp(storage: Storage, config: Config) {
|
||||
const expiresAt = Date.now() + offerTtl;
|
||||
|
||||
// Create offer first
|
||||
const offers = await storage.createOffers([{
|
||||
offers = await storage.createOffers([{
|
||||
peerId,
|
||||
sdp,
|
||||
expiresAt
|
||||
@@ -277,6 +291,7 @@ export function createApp(storage: Storage, config: Config) {
|
||||
/**
|
||||
* GET /services/:uuid
|
||||
* Get service details by index UUID
|
||||
* Returns an available (unanswered) offer from the service's pool
|
||||
*/
|
||||
app.get('/services/:uuid', async (c) => {
|
||||
try {
|
||||
@@ -288,19 +303,32 @@ export function createApp(storage: Storage, config: Config) {
|
||||
return c.json({ error: 'Service not found' }, 404);
|
||||
}
|
||||
|
||||
// Get associated offer
|
||||
const offer = await storage.getOfferById(service.offerId);
|
||||
// Get the initial offer to find the peer ID
|
||||
const initialOffer = await storage.getOfferById(service.offerId);
|
||||
|
||||
if (!offer) {
|
||||
if (!initialOffer) {
|
||||
return c.json({ error: 'Associated offer not found' }, 404);
|
||||
}
|
||||
|
||||
// Get all offers from this peer
|
||||
const peerOffers = await storage.getOffersByPeerId(initialOffer.peerId);
|
||||
|
||||
// Find an unanswered offer
|
||||
const availableOffer = peerOffers.find(offer => !offer.answererPeerId);
|
||||
|
||||
if (!availableOffer) {
|
||||
return c.json({
|
||||
error: 'No available offers',
|
||||
message: 'All offers from this service are currently in use. Please try again later.'
|
||||
}, 503);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
serviceId: service.id,
|
||||
username: service.username,
|
||||
serviceFqn: service.serviceFqn,
|
||||
offerId: service.offerId,
|
||||
sdp: offer.sdp,
|
||||
offerId: availableOffer.id,
|
||||
sdp: availableOffer.sdp,
|
||||
isPublic: service.isPublic,
|
||||
metadata: service.metadata ? JSON.parse(service.metadata) : undefined,
|
||||
createdAt: service.createdAt,
|
||||
@@ -530,8 +558,8 @@ export function createApp(storage: Storage, config: Config) {
|
||||
return c.json({
|
||||
answers: offers.map(offer => ({
|
||||
offerId: offer.id,
|
||||
answererPeerId: offer.answererPeerId,
|
||||
answerSdp: offer.answerSdp,
|
||||
answererId: offer.answererPeerId,
|
||||
sdp: offer.answerSdp,
|
||||
answeredAt: offer.answeredAt
|
||||
}))
|
||||
}, 200);
|
||||
|
||||
66
src/bloom.ts
66
src/bloom.ts
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Bloom filter utility for testing if peer IDs might be in a set
|
||||
* Used to filter out known peers from discovery results
|
||||
*/
|
||||
|
||||
export class BloomFilter {
|
||||
private bits: Uint8Array;
|
||||
private size: number;
|
||||
private numHashes: number;
|
||||
|
||||
/**
|
||||
* Creates a bloom filter from a base64 encoded bit array
|
||||
*/
|
||||
constructor(base64Data: string, numHashes: number = 3) {
|
||||
// Decode base64 to Uint8Array (works in both Node.js and Workers)
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
this.bits = bytes;
|
||||
this.size = this.bits.length * 8;
|
||||
this.numHashes = numHashes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a peer ID might be in the filter
|
||||
* Returns true if possibly in set, false if definitely not in set
|
||||
*/
|
||||
test(peerId: string): boolean {
|
||||
for (let i = 0; i < this.numHashes; i++) {
|
||||
const hash = this.hash(peerId, i);
|
||||
const index = hash % this.size;
|
||||
const byteIndex = Math.floor(index / 8);
|
||||
const bitIndex = index % 8;
|
||||
|
||||
if (!(this.bits[byteIndex] & (1 << bitIndex))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple hash function (FNV-1a variant)
|
||||
*/
|
||||
private hash(str: string, seed: number): number {
|
||||
let hash = 2166136261 ^ seed;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash ^= str.charCodeAt(i);
|
||||
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to parse bloom filter from base64 string
|
||||
*/
|
||||
export function parseBloomFilter(base64: string): BloomFilter | null {
|
||||
try {
|
||||
return new BloomFilter(base64);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export interface Config {
|
||||
offerMinTtl: number;
|
||||
cleanupInterval: number;
|
||||
maxOffersPerRequest: number;
|
||||
maxTopicsPerOffer: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +44,6 @@ export function loadConfig(): Config {
|
||||
offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || '86400000', 10),
|
||||
offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || '60000', 10),
|
||||
cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || '60000', 10),
|
||||
maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10),
|
||||
maxTopicsPerOffer: parseInt(process.env.MAX_TOPICS_PER_OFFER || '50', 10),
|
||||
maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ async function main() {
|
||||
offerMinTtl: `${config.offerMinTtl}ms`,
|
||||
cleanupInterval: `${config.cleanupInterval}ms`,
|
||||
maxOffersPerRequest: config.maxOffersPerRequest,
|
||||
maxTopicsPerOffer: config.maxTopicsPerOffer,
|
||||
corsOrigins: config.corsOrigins,
|
||||
version: config.version,
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export class D1Storage implements Storage {
|
||||
*/
|
||||
async initializeDatabase(): Promise<void> {
|
||||
await this.db.exec(`
|
||||
-- Offers table (no topics)
|
||||
-- WebRTC signaling offers
|
||||
CREATE TABLE IF NOT EXISTS offers (
|
||||
id TEXT PRIMARY KEY,
|
||||
peer_id TEXT NOT NULL,
|
||||
@@ -125,7 +125,7 @@ export class D1Storage implements Storage {
|
||||
|
||||
// D1 doesn't support true transactions yet, so we do this sequentially
|
||||
for (const offer of offers) {
|
||||
const id = offer.id || await generateOfferHash(offer.sdp, []);
|
||||
const id = offer.id || await generateOfferHash(offer.sdp);
|
||||
const now = Date.now();
|
||||
|
||||
await this.db.prepare(`
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
/**
|
||||
* Generates a content-based offer ID using SHA-256 hash
|
||||
* Creates deterministic IDs based on offer content (sdp, topics)
|
||||
* Creates deterministic IDs based on offer SDP content
|
||||
* PeerID is not included as it's inferred from authentication
|
||||
* Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers
|
||||
*
|
||||
* @param sdp - The WebRTC SDP offer
|
||||
* @param topics - Array of topic strings
|
||||
* @returns SHA-256 hash of the sanitized offer content
|
||||
* @returns SHA-256 hash of the SDP content
|
||||
*/
|
||||
export async function generateOfferHash(
|
||||
sdp: string,
|
||||
topics: string[]
|
||||
): Promise<string> {
|
||||
export async function generateOfferHash(sdp: string): Promise<string> {
|
||||
// Sanitize and normalize the offer content
|
||||
// Only include core offer content (not peerId - that's inferred from auth)
|
||||
const sanitizedOffer = {
|
||||
sdp,
|
||||
topics: [...topics].sort(), // Sort topics for consistency
|
||||
sdp
|
||||
};
|
||||
|
||||
// Create non-prettified JSON string
|
||||
|
||||
@@ -36,7 +36,7 @@ export class SQLiteStorage implements Storage {
|
||||
*/
|
||||
private initializeDatabase(): void {
|
||||
this.db.exec(`
|
||||
-- Offers table (no topics)
|
||||
-- WebRTC signaling offers
|
||||
CREATE TABLE IF NOT EXISTS offers (
|
||||
id TEXT PRIMARY KEY,
|
||||
peer_id TEXT NOT NULL,
|
||||
@@ -132,7 +132,7 @@ export class SQLiteStorage implements Storage {
|
||||
const offersWithIds = await Promise.all(
|
||||
offers.map(async (offer) => ({
|
||||
...offer,
|
||||
id: offer.id || await generateOfferHash(offer.sdp, []),
|
||||
id: offer.id || await generateOfferHash(offer.sdp),
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Represents a WebRTC signaling offer (no topics)
|
||||
* Represents a WebRTC signaling offer
|
||||
*/
|
||||
export interface Offer {
|
||||
id: string;
|
||||
@@ -9,7 +9,6 @@ export interface Offer {
|
||||
expiresAt: number;
|
||||
lastSeen: number;
|
||||
secret?: string;
|
||||
info?: string;
|
||||
answererPeerId?: string;
|
||||
answerSdp?: string;
|
||||
answeredAt?: number;
|
||||
@@ -37,7 +36,6 @@ export interface CreateOfferRequest {
|
||||
sdp: string;
|
||||
expiresAt: number;
|
||||
secret?: string;
|
||||
info?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,6 @@ export interface Env {
|
||||
OFFER_MAX_TTL?: string;
|
||||
OFFER_MIN_TTL?: string;
|
||||
MAX_OFFERS_PER_REQUEST?: string;
|
||||
MAX_TOPICS_PER_OFFER?: string;
|
||||
CORS_ORIGINS?: string;
|
||||
VERSION?: string;
|
||||
}
|
||||
@@ -43,8 +42,7 @@ export default {
|
||||
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
|
||||
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
|
||||
cleanupInterval: 60000, // Not used in Workers (scheduled handler instead)
|
||||
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100,
|
||||
maxTopicsPerOffer: env.MAX_TOPICS_PER_OFFER ? parseInt(env.MAX_TOPICS_PER_OFFER, 10) : 50,
|
||||
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100
|
||||
};
|
||||
|
||||
// Create Hono app
|
||||
|
||||
Reference in New Issue
Block a user