Expand README with links to related repositories and NPM packages

This commit is contained in:
2025-11-17 21:41:55 +01:00
parent 8a65626225
commit 7ca42c42aa
5 changed files with 63 additions and 15 deletions

View File

@@ -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))
--- ---

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;
}>; }>;