mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-10 02:43:24 +00:00
Expand README with links to related repositories and NPM packages
This commit is contained in:
@@ -5,8 +5,9 @@
|
|||||||
Scalable peer-to-peer connection establishment with topic-based discovery, stateless authentication, and complete WebRTC signaling.
|
Scalable peer-to-peer connection establishment with topic-based discovery, stateless authentication, and complete WebRTC signaling.
|
||||||
|
|
||||||
**Related repositories:**
|
**Related repositories:**
|
||||||
- [@xtr-dev/rondevu-client](https://www.npmjs.com/package/@xtr-dev/rondevu-client) - TypeScript client library
|
- [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
|
||||||
- [rondevu-demo](https://rondevu-demo.pages.dev) - Interactive demo
|
- [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server))
|
||||||
|
- [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://rondevu-demo.pages.dev))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
27
src/app.ts
27
src/app.ts
@@ -115,6 +115,16 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
return c.json({ error: 'SDP must be 64KB or less' }, 400);
|
return c.json({ error: 'SDP must be 64KB or less' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate secret if provided
|
||||||
|
if (offer.secret !== undefined) {
|
||||||
|
if (typeof offer.secret !== 'string') {
|
||||||
|
return c.json({ error: 'Secret must be a string' }, 400);
|
||||||
|
}
|
||||||
|
if (offer.secret.length > 128) {
|
||||||
|
return c.json({ error: 'Secret must be 128 characters or less' }, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate topics
|
// Validate topics
|
||||||
if (!Array.isArray(offer.topics) || offer.topics.length === 0) {
|
if (!Array.isArray(offer.topics) || offer.topics.length === 0) {
|
||||||
return c.json({ error: 'Each offer must have a non-empty topics array' }, 400);
|
return c.json({ error: 'Each offer must have a non-empty topics array' }, 400);
|
||||||
@@ -145,6 +155,7 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
sdp: offer.sdp,
|
sdp: offer.sdp,
|
||||||
topics: offer.topics,
|
topics: offer.topics,
|
||||||
expiresAt: Date.now() + ttl,
|
expiresAt: Date.now() + ttl,
|
||||||
|
secret: offer.secret,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +227,8 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
sdp: o.sdp,
|
sdp: o.sdp,
|
||||||
topics: o.topics,
|
topics: o.topics,
|
||||||
expiresAt: o.expiresAt,
|
expiresAt: o.expiresAt,
|
||||||
lastSeen: o.lastSeen
|
lastSeen: o.lastSeen,
|
||||||
|
hasSecret: !!o.secret // Indicate if secret is required without exposing it
|
||||||
})),
|
})),
|
||||||
total: bloomParam ? total + excludePeerIds.length : total,
|
total: bloomParam ? total + excludePeerIds.length : total,
|
||||||
returned: offers.length
|
returned: offers.length
|
||||||
@@ -282,7 +294,8 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
sdp: o.sdp,
|
sdp: o.sdp,
|
||||||
topics: o.topics,
|
topics: o.topics,
|
||||||
expiresAt: o.expiresAt,
|
expiresAt: o.expiresAt,
|
||||||
lastSeen: o.lastSeen
|
lastSeen: o.lastSeen,
|
||||||
|
hasSecret: !!o.secret // Indicate if secret is required without exposing it
|
||||||
})),
|
})),
|
||||||
topics: Array.from(topicsSet)
|
topics: Array.from(topicsSet)
|
||||||
}, 200);
|
}, 200);
|
||||||
@@ -311,6 +324,7 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
createdAt: o.createdAt,
|
createdAt: o.createdAt,
|
||||||
expiresAt: o.expiresAt,
|
expiresAt: o.expiresAt,
|
||||||
lastSeen: o.lastSeen,
|
lastSeen: o.lastSeen,
|
||||||
|
secret: o.secret, // Owner can see the secret
|
||||||
answererPeerId: o.answererPeerId,
|
answererPeerId: o.answererPeerId,
|
||||||
answeredAt: o.answeredAt
|
answeredAt: o.answeredAt
|
||||||
}))
|
}))
|
||||||
@@ -354,7 +368,7 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
const offerId = c.req.param('offerId');
|
const offerId = c.req.param('offerId');
|
||||||
const peerId = getAuthenticatedPeerId(c);
|
const peerId = getAuthenticatedPeerId(c);
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { sdp } = body;
|
const { sdp, secret } = body;
|
||||||
|
|
||||||
if (!sdp || typeof sdp !== 'string') {
|
if (!sdp || typeof sdp !== 'string') {
|
||||||
return c.json({ error: 'Missing or invalid required parameter: sdp' }, 400);
|
return c.json({ error: 'Missing or invalid required parameter: sdp' }, 400);
|
||||||
@@ -364,7 +378,12 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
return c.json({ error: 'SDP must be 64KB or less' }, 400);
|
return c.json({ error: 'SDP must be 64KB or less' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await storage.answerOffer(offerId, peerId, sdp);
|
// Validate secret if provided
|
||||||
|
if (secret !== undefined && typeof secret !== 'string') {
|
||||||
|
return c.json({ error: 'Secret must be a string' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await storage.answerOffer(offerId, peerId, sdp, secret);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return c.json({ error: result.error }, 400);
|
return c.json({ error: result.error }, 400);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class D1Storage implements Storage {
|
|||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
expires_at INTEGER NOT NULL,
|
expires_at INTEGER NOT NULL,
|
||||||
last_seen INTEGER NOT NULL,
|
last_seen INTEGER NOT NULL,
|
||||||
|
secret TEXT,
|
||||||
answerer_peer_id TEXT,
|
answerer_peer_id TEXT,
|
||||||
answer_sdp TEXT,
|
answer_sdp TEXT,
|
||||||
answered_at INTEGER
|
answered_at INTEGER
|
||||||
@@ -75,9 +76,9 @@ export class D1Storage implements Storage {
|
|||||||
|
|
||||||
// Insert offer
|
// Insert offer
|
||||||
await this.db.prepare(`
|
await this.db.prepare(`
|
||||||
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen)
|
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`).bind(id, offer.peerId, offer.sdp, now, offer.expiresAt, now).run();
|
`).bind(id, offer.peerId, offer.sdp, now, offer.expiresAt, now, offer.secret || null).run();
|
||||||
|
|
||||||
// Insert topics
|
// Insert topics
|
||||||
for (const topic of offer.topics) {
|
for (const topic of offer.topics) {
|
||||||
@@ -95,6 +96,7 @@ export class D1Storage implements Storage {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
expiresAt: offer.expiresAt,
|
expiresAt: offer.expiresAt,
|
||||||
lastSeen: now,
|
lastSeen: now,
|
||||||
|
secret: offer.secret,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +177,8 @@ export class D1Storage implements Storage {
|
|||||||
async answerOffer(
|
async answerOffer(
|
||||||
offerId: string,
|
offerId: string,
|
||||||
answererPeerId: string,
|
answererPeerId: string,
|
||||||
answerSdp: string
|
answerSdp: string,
|
||||||
|
secret?: string
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
// Check if offer exists and is not expired
|
// Check if offer exists and is not expired
|
||||||
const offer = await this.getOfferById(offerId);
|
const offer = await this.getOfferById(offerId);
|
||||||
@@ -187,6 +190,14 @@ export class D1Storage implements Storage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify secret if offer is protected
|
||||||
|
if (offer.secret && offer.secret !== secret) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid or missing secret'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Check if offer already has an answerer
|
// Check if offer already has an answerer
|
||||||
if (offer.answererPeerId) {
|
if (offer.answererPeerId) {
|
||||||
return {
|
return {
|
||||||
@@ -374,6 +385,7 @@ export class D1Storage implements Storage {
|
|||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
expiresAt: row.expires_at,
|
expiresAt: row.expires_at,
|
||||||
lastSeen: row.last_seen,
|
lastSeen: row.last_seen,
|
||||||
|
secret: row.secret || undefined,
|
||||||
answererPeerId: row.answerer_peer_id || undefined,
|
answererPeerId: row.answerer_peer_id || undefined,
|
||||||
answerSdp: row.answer_sdp || undefined,
|
answerSdp: row.answer_sdp || undefined,
|
||||||
answeredAt: row.answered_at || undefined,
|
answeredAt: row.answered_at || undefined,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class SQLiteStorage implements Storage {
|
|||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
expires_at INTEGER NOT NULL,
|
expires_at INTEGER NOT NULL,
|
||||||
last_seen INTEGER NOT NULL,
|
last_seen INTEGER NOT NULL,
|
||||||
|
secret TEXT,
|
||||||
answerer_peer_id TEXT,
|
answerer_peer_id TEXT,
|
||||||
answer_sdp TEXT,
|
answer_sdp TEXT,
|
||||||
answered_at INTEGER
|
answered_at INTEGER
|
||||||
@@ -83,8 +84,8 @@ export class SQLiteStorage implements Storage {
|
|||||||
// Use transaction for atomic creation
|
// Use transaction for atomic creation
|
||||||
const transaction = this.db.transaction((offersWithIds: (CreateOfferRequest & { id: string })[]) => {
|
const transaction = this.db.transaction((offersWithIds: (CreateOfferRequest & { id: string })[]) => {
|
||||||
const offerStmt = this.db.prepare(`
|
const offerStmt = this.db.prepare(`
|
||||||
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen)
|
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const topicStmt = this.db.prepare(`
|
const topicStmt = this.db.prepare(`
|
||||||
@@ -102,7 +103,8 @@ export class SQLiteStorage implements Storage {
|
|||||||
offer.sdp,
|
offer.sdp,
|
||||||
now,
|
now,
|
||||||
offer.expiresAt,
|
offer.expiresAt,
|
||||||
now
|
now,
|
||||||
|
offer.secret || null
|
||||||
);
|
);
|
||||||
|
|
||||||
// Insert topics
|
// Insert topics
|
||||||
@@ -118,6 +120,7 @@ export class SQLiteStorage implements Storage {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
expiresAt: offer.expiresAt,
|
expiresAt: offer.expiresAt,
|
||||||
lastSeen: now,
|
lastSeen: now,
|
||||||
|
secret: offer.secret,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -195,7 +198,8 @@ export class SQLiteStorage implements Storage {
|
|||||||
async answerOffer(
|
async answerOffer(
|
||||||
offerId: string,
|
offerId: string,
|
||||||
answererPeerId: string,
|
answererPeerId: string,
|
||||||
answerSdp: string
|
answerSdp: string,
|
||||||
|
secret?: string
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
// Check if offer exists and is not expired
|
// Check if offer exists and is not expired
|
||||||
const offer = await this.getOfferById(offerId);
|
const offer = await this.getOfferById(offerId);
|
||||||
@@ -207,6 +211,14 @@ export class SQLiteStorage implements Storage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify secret if offer is protected
|
||||||
|
if (offer.secret && offer.secret !== secret) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid or missing secret'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Check if offer already has an answerer
|
// Check if offer already has an answerer
|
||||||
if (offer.answererPeerId) {
|
if (offer.answererPeerId) {
|
||||||
return {
|
return {
|
||||||
@@ -382,6 +394,7 @@ export class SQLiteStorage implements Storage {
|
|||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
expiresAt: row.expires_at,
|
expiresAt: row.expires_at,
|
||||||
lastSeen: row.last_seen,
|
lastSeen: row.last_seen,
|
||||||
|
secret: row.secret || undefined,
|
||||||
answererPeerId: row.answerer_peer_id || undefined,
|
answererPeerId: row.answerer_peer_id || undefined,
|
||||||
answerSdp: row.answer_sdp || undefined,
|
answerSdp: row.answer_sdp || undefined,
|
||||||
answeredAt: row.answered_at || undefined,
|
answeredAt: row.answered_at || undefined,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface Offer {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
lastSeen: number;
|
lastSeen: number;
|
||||||
|
secret?: string;
|
||||||
answererPeerId?: string;
|
answererPeerId?: string;
|
||||||
answerSdp?: string;
|
answerSdp?: string;
|
||||||
answeredAt?: number;
|
answeredAt?: number;
|
||||||
@@ -44,6 +45,7 @@ export interface CreateOfferRequest {
|
|||||||
sdp: string;
|
sdp: string;
|
||||||
topics: string[];
|
topics: string[];
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
|
secret?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,9 +102,10 @@ export interface Storage {
|
|||||||
* @param offerId Offer identifier
|
* @param offerId Offer identifier
|
||||||
* @param answererPeerId Answerer's peer ID
|
* @param answererPeerId Answerer's peer ID
|
||||||
* @param answerSdp WebRTC answer SDP
|
* @param answerSdp WebRTC answer SDP
|
||||||
|
* @param secret Optional secret for protected offers
|
||||||
* @returns Success status and optional error message
|
* @returns Success status and optional error message
|
||||||
*/
|
*/
|
||||||
answerOffer(offerId: string, answererPeerId: string, answerSdp: string): Promise<{
|
answerOffer(offerId: string, answererPeerId: string, answerSdp: string, secret?: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
Reference in New Issue
Block a user