mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-10 10:53:24 +00:00
Remove origin-based session bucketing
- Remove origin column from offers table - Remove getOrigin() function and X-Rondevu-Global header support - Simplify all storage methods to use only offer code - Add migration 0003_remove_origin.sql - Update error messages to remove "origin mismatch" wording This simplifies the system by removing origin-based isolation. All offers are now identified solely by their unique code/ID. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
29
migrations/0003_remove_origin.sql
Normal file
29
migrations/0003_remove_origin.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-- Migration: Remove origin column from offers table
|
||||||
|
-- This simplifies offer lookup to only use offer codes
|
||||||
|
-- Origin-based bucketing is no longer needed
|
||||||
|
|
||||||
|
-- Create new offers table without origin column
|
||||||
|
CREATE TABLE offers_new (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
||||||
|
offer TEXT NOT NULL,
|
||||||
|
answer TEXT,
|
||||||
|
offer_candidates TEXT NOT NULL DEFAULT '[]',
|
||||||
|
answer_candidates TEXT NOT NULL DEFAULT '[]',
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
expires_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Copy data from old table
|
||||||
|
INSERT INTO offers_new (code, peer_id, offer, answer, offer_candidates, answer_candidates, created_at, expires_at)
|
||||||
|
SELECT code, peer_id, offer, answer, offer_candidates, answer_candidates, created_at, expires_at
|
||||||
|
FROM offers;
|
||||||
|
|
||||||
|
-- Drop old table
|
||||||
|
DROP TABLE offers;
|
||||||
|
|
||||||
|
-- Rename new table
|
||||||
|
ALTER TABLE offers_new RENAME TO offers;
|
||||||
|
|
||||||
|
-- Recreate index
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_offers_expires_at ON offers(expires_at);
|
||||||
34
src/app.ts
34
src/app.ts
@@ -9,19 +9,6 @@ export interface AppConfig {
|
|||||||
version?: string;
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the origin for offer isolation.
|
|
||||||
* If X-Rondevu-Global header is set to 'true', returns the global origin (https://ronde.vu).
|
|
||||||
* Otherwise, returns the request's Origin header.
|
|
||||||
*/
|
|
||||||
function getOrigin(c: Context): string {
|
|
||||||
const globalHeader = c.req.header('X-Rondevu-Global');
|
|
||||||
if (globalHeader === 'true') {
|
|
||||||
return 'https://ronde.vu';
|
|
||||||
}
|
|
||||||
return c.req.header('Origin') || c.req.header('origin') || 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the Hono application with WebRTC signaling endpoints
|
* Creates the Hono application with WebRTC signaling endpoints
|
||||||
*/
|
*/
|
||||||
@@ -43,7 +30,7 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
return config.corsOrigins[0];
|
return config.corsOrigins[0];
|
||||||
},
|
},
|
||||||
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
||||||
allowHeaders: ['Content-Type', 'Origin', 'X-Rondevu-Global'],
|
allowHeaders: ['Content-Type', 'Origin'],
|
||||||
exposeHeaders: ['Content-Type'],
|
exposeHeaders: ['Content-Type'],
|
||||||
maxAge: 600,
|
maxAge: 600,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
@@ -78,7 +65,6 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
*/
|
*/
|
||||||
app.post('/offer', async (c) => {
|
app.post('/offer', async (c) => {
|
||||||
try {
|
try {
|
||||||
const origin = getOrigin(c);
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { peerId, offer, code: customCode } = body;
|
const { peerId, offer, code: customCode } = body;
|
||||||
|
|
||||||
@@ -95,7 +81,7 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = Date.now() + config.offerTimeout;
|
const expiresAt = Date.now() + config.offerTimeout;
|
||||||
const code = await storage.createOffer(origin, peerId, offer, expiresAt, customCode);
|
const code = await storage.createOffer(peerId, offer, expiresAt, customCode);
|
||||||
|
|
||||||
return c.json({ code }, 200);
|
return c.json({ code }, 200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -117,7 +103,6 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
*/
|
*/
|
||||||
app.post('/answer', async (c) => {
|
app.post('/answer', async (c) => {
|
||||||
try {
|
try {
|
||||||
const origin = getOrigin(c);
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { code, answer, candidate, side } = body;
|
const { code, answer, candidate, side } = body;
|
||||||
|
|
||||||
@@ -137,23 +122,23 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
return c.json({ error: 'Cannot provide both answer and candidate' }, 400);
|
return c.json({ error: 'Cannot provide both answer and candidate' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = await storage.getOffer(code, origin);
|
const offer = await storage.getOffer(code);
|
||||||
|
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
return c.json({ error: 'Offer not found, expired, or origin mismatch' }, 404);
|
return c.json({ error: 'Offer not found or expired' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answer) {
|
if (answer) {
|
||||||
await storage.updateOffer(code, origin, { answer });
|
await storage.updateOffer(code, { answer });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
if (side === 'offerer') {
|
if (side === 'offerer') {
|
||||||
const updatedCandidates = [...offer.offerCandidates, candidate];
|
const updatedCandidates = [...offer.offerCandidates, candidate];
|
||||||
await storage.updateOffer(code, origin, { offerCandidates: updatedCandidates });
|
await storage.updateOffer(code, { offerCandidates: updatedCandidates });
|
||||||
} else {
|
} else {
|
||||||
const updatedCandidates = [...offer.answerCandidates, candidate];
|
const updatedCandidates = [...offer.answerCandidates, candidate];
|
||||||
await storage.updateOffer(code, origin, { answerCandidates: updatedCandidates });
|
await storage.updateOffer(code, { answerCandidates: updatedCandidates });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +156,6 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
*/
|
*/
|
||||||
app.post('/poll', async (c) => {
|
app.post('/poll', async (c) => {
|
||||||
try {
|
try {
|
||||||
const origin = getOrigin(c);
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { code, side } = body;
|
const { code, side } = body;
|
||||||
|
|
||||||
@@ -183,10 +167,10 @@ export function createApp(storage: Storage, config: AppConfig) {
|
|||||||
return c.json({ error: 'Invalid or missing parameter: side (must be "offerer" or "answerer")' }, 400);
|
return c.json({ error: 'Invalid or missing parameter: side (must be "offerer" or "answerer")' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = await storage.getOffer(code, origin);
|
const offer = await storage.getOffer(code);
|
||||||
|
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
return c.json({ error: 'Offer not found, expired, or origin mismatch' }, 404);
|
return c.json({ error: 'Offer not found or expired' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (side === 'offerer') {
|
if (side === 'offerer') {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export class D1Storage implements Storage {
|
|||||||
await this.db.exec(`
|
await this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS offers (
|
CREATE TABLE IF NOT EXISTS offers (
|
||||||
code TEXT PRIMARY KEY,
|
code TEXT PRIMARY KEY,
|
||||||
origin TEXT NOT NULL,
|
|
||||||
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
||||||
offer TEXT NOT NULL,
|
offer TEXT NOT NULL,
|
||||||
answer TEXT,
|
answer TEXT,
|
||||||
@@ -38,12 +37,10 @@ export class D1Storage implements Storage {
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_offers_expires_at ON offers(expires_at);
|
CREATE INDEX IF NOT EXISTS idx_offers_expires_at ON offers(expires_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_offers_origin ON offers(origin);
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOffer(
|
async createOffer(
|
||||||
origin: string,
|
|
||||||
peerId: string,
|
peerId: string,
|
||||||
offer: string,
|
offer: string,
|
||||||
expiresAt: number,
|
expiresAt: number,
|
||||||
@@ -64,9 +61,9 @@ export class D1Storage implements Storage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.db.prepare(`
|
await this.db.prepare(`
|
||||||
INSERT INTO offers (code, origin, peer_id, offer, created_at, expires_at)
|
INSERT INTO offers (code, peer_id, offer, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`).bind(code, origin, peerId, offer, Date.now(), expiresAt).run();
|
`).bind(code, peerId, offer, Date.now(), expiresAt).run();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -85,12 +82,12 @@ export class D1Storage implements Storage {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOffer(code: string, origin: string): Promise<Offer | null> {
|
async getOffer(code: string): Promise<Offer | null> {
|
||||||
try {
|
try {
|
||||||
const result = await this.db.prepare(`
|
const result = await this.db.prepare(`
|
||||||
SELECT * FROM offers
|
SELECT * FROM offers
|
||||||
WHERE code = ? AND origin = ? AND expires_at > ?
|
WHERE code = ? AND expires_at > ?
|
||||||
`).bind(code, origin, Date.now()).first();
|
`).bind(code, Date.now()).first();
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
@@ -100,7 +97,6 @@ export class D1Storage implements Storage {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
code: row.code,
|
code: row.code,
|
||||||
origin: row.origin,
|
|
||||||
peerId: row.peer_id,
|
peerId: row.peer_id,
|
||||||
offer: row.offer,
|
offer: row.offer,
|
||||||
answer: row.answer || undefined,
|
answer: row.answer || undefined,
|
||||||
@@ -115,12 +111,12 @@ export class D1Storage implements Storage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void> {
|
async updateOffer(code: string, update: Partial<Offer>): Promise<void> {
|
||||||
// Verify offer exists and origin matches
|
// Verify offer exists
|
||||||
const current = await this.getOffer(code, origin);
|
const current = await this.getOffer(code);
|
||||||
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Offer not found or origin mismatch');
|
throw new Error('Offer not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build update query dynamically based on what fields are being updated
|
// Build update query dynamically based on what fields are being updated
|
||||||
@@ -147,13 +143,13 @@ export class D1Storage implements Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add WHERE clause values
|
// Add WHERE clause values
|
||||||
values.push(code, origin);
|
values.push(code);
|
||||||
|
|
||||||
// D1 provides strong consistency, so this update is atomic and immediately visible
|
// D1 provides strong consistency, so this update is atomic and immediately visible
|
||||||
const query = `
|
const query = `
|
||||||
UPDATE offers
|
UPDATE offers
|
||||||
SET ${updates.join(', ')}
|
SET ${updates.join(', ')}
|
||||||
WHERE code = ? AND origin = ?
|
WHERE code = ?
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await this.db.prepare(query).bind(...values).run();
|
await this.db.prepare(query).bind(...values).run();
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export class SQLiteStorage implements Storage {
|
|||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS offers (
|
CREATE TABLE IF NOT EXISTS offers (
|
||||||
code TEXT PRIMARY KEY,
|
code TEXT PRIMARY KEY,
|
||||||
origin TEXT NOT NULL,
|
|
||||||
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
|
||||||
offer TEXT NOT NULL,
|
offer TEXT NOT NULL,
|
||||||
answer TEXT,
|
answer TEXT,
|
||||||
@@ -37,7 +36,6 @@ export class SQLiteStorage implements Storage {
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_offers_expires_at ON offers(expires_at);
|
CREATE INDEX IF NOT EXISTS idx_offers_expires_at ON offers(expires_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_offers_origin ON offers(origin);
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +58,7 @@ export class SQLiteStorage implements Storage {
|
|||||||
return randomUUID();
|
return randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOffer(origin: string, peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string> {
|
async createOffer(peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string> {
|
||||||
// Validate peerId length
|
// Validate peerId length
|
||||||
if (peerId.length > 1024) {
|
if (peerId.length > 1024) {
|
||||||
throw new Error('PeerId string must be 1024 characters or less');
|
throw new Error('PeerId string must be 1024 characters or less');
|
||||||
@@ -81,11 +79,11 @@ export class SQLiteStorage implements Storage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
INSERT INTO offers (code, origin, peer_id, offer, created_at, expires_at)
|
INSERT INTO offers (code, peer_id, offer, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
stmt.run(code, origin, peerId, offer, Date.now(), expiresAt);
|
stmt.run(code, peerId, offer, Date.now(), expiresAt);
|
||||||
break;
|
break;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// If unique constraint failed with custom code, throw error
|
// If unique constraint failed with custom code, throw error
|
||||||
@@ -103,12 +101,12 @@ export class SQLiteStorage implements Storage {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOffer(code: string, origin: string): Promise<Offer | null> {
|
async getOffer(code: string): Promise<Offer | null> {
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT * FROM offers WHERE code = ? AND origin = ? AND expires_at > ?
|
SELECT * FROM offers WHERE code = ? AND expires_at > ?
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const row = stmt.get(code, origin, Date.now()) as any;
|
const row = stmt.get(code, Date.now()) as any;
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
@@ -116,7 +114,6 @@ export class SQLiteStorage implements Storage {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
code: row.code,
|
code: row.code,
|
||||||
origin: row.origin,
|
|
||||||
peerId: row.peer_id,
|
peerId: row.peer_id,
|
||||||
offer: row.offer,
|
offer: row.offer,
|
||||||
answer: row.answer || undefined,
|
answer: row.answer || undefined,
|
||||||
@@ -127,11 +124,11 @@ export class SQLiteStorage implements Storage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void> {
|
async updateOffer(code: string, update: Partial<Offer>): Promise<void> {
|
||||||
const current = await this.getOffer(code, origin);
|
const current = await this.getOffer(code);
|
||||||
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Offer not found or origin mismatch');
|
throw new Error('Offer not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates: string[] = [];
|
const updates: string[] = [];
|
||||||
@@ -157,10 +154,9 @@ export class SQLiteStorage implements Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
values.push(code);
|
values.push(code);
|
||||||
values.push(origin);
|
|
||||||
|
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
UPDATE offers SET ${updates.join(', ')} WHERE code = ? AND origin = ?
|
UPDATE offers SET ${updates.join(', ')} WHERE code = ?
|
||||||
`);
|
`);
|
||||||
|
|
||||||
stmt.run(...values);
|
stmt.run(...values);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export interface Offer {
|
export interface Offer {
|
||||||
code: string;
|
code: string;
|
||||||
origin: string;
|
|
||||||
peerId: string;
|
peerId: string;
|
||||||
offer: string;
|
offer: string;
|
||||||
answer?: string;
|
answer?: string;
|
||||||
@@ -20,30 +19,27 @@ export interface Offer {
|
|||||||
export interface Storage {
|
export interface Storage {
|
||||||
/**
|
/**
|
||||||
* Creates a new offer
|
* Creates a new offer
|
||||||
* @param origin The Origin header from the request
|
|
||||||
* @param peerId Peer identifier string (max 1024 chars)
|
* @param peerId Peer identifier string (max 1024 chars)
|
||||||
* @param offer The WebRTC SDP offer message
|
* @param offer The WebRTC SDP offer message
|
||||||
* @param expiresAt Unix timestamp when the offer should expire
|
* @param expiresAt Unix timestamp when the offer should expire
|
||||||
* @param customCode Optional custom code (if not provided, generates UUID)
|
* @param customCode Optional custom code (if not provided, generates UUID)
|
||||||
* @returns The unique offer code
|
* @returns The unique offer code
|
||||||
*/
|
*/
|
||||||
createOffer(origin: string, peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string>;
|
createOffer(peerId: string, offer: string, expiresAt: number, customCode?: string): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves an offer by its code
|
* Retrieves an offer by its code
|
||||||
* @param code The offer code
|
* @param code The offer code
|
||||||
* @param origin The Origin header from the request (for validation)
|
|
||||||
* @returns The offer if found, null otherwise
|
* @returns The offer if found, null otherwise
|
||||||
*/
|
*/
|
||||||
getOffer(code: string, origin: string): Promise<Offer | null>;
|
getOffer(code: string): Promise<Offer | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an existing offer with new data
|
* Updates an existing offer with new data
|
||||||
* @param code The offer code
|
* @param code The offer code
|
||||||
* @param origin The Origin header from the request (for validation)
|
|
||||||
* @param update Partial offer data to update
|
* @param update Partial offer data to update
|
||||||
*/
|
*/
|
||||||
updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void>;
|
updateOffer(code: string, update: Partial<Offer>): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an offer
|
* Deletes an offer
|
||||||
|
|||||||
Reference in New Issue
Block a user