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:
2025-11-12 23:59:03 +01:00
parent cdc66ad1d7
commit 8a94b661a8
5 changed files with 64 additions and 63 deletions

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

View File

@@ -9,19 +9,6 @@ export interface AppConfig {
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
*/
@@ -43,7 +30,7 @@ export function createApp(storage: Storage, config: AppConfig) {
return config.corsOrigins[0];
},
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Origin', 'X-Rondevu-Global'],
allowHeaders: ['Content-Type', 'Origin'],
exposeHeaders: ['Content-Type'],
maxAge: 600,
credentials: true,
@@ -78,7 +65,6 @@ export function createApp(storage: Storage, config: AppConfig) {
*/
app.post('/offer', async (c) => {
try {
const origin = getOrigin(c);
const body = await c.req.json();
const { peerId, offer, code: customCode } = body;
@@ -95,7 +81,7 @@ export function createApp(storage: Storage, config: AppConfig) {
}
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);
} catch (err) {
@@ -117,7 +103,6 @@ export function createApp(storage: Storage, config: AppConfig) {
*/
app.post('/answer', async (c) => {
try {
const origin = getOrigin(c);
const body = await c.req.json();
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);
}
const offer = await storage.getOffer(code, origin);
const offer = await storage.getOffer(code);
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) {
await storage.updateOffer(code, origin, { answer });
await storage.updateOffer(code, { answer });
}
if (candidate) {
if (side === 'offerer') {
const updatedCandidates = [...offer.offerCandidates, candidate];
await storage.updateOffer(code, origin, { offerCandidates: updatedCandidates });
await storage.updateOffer(code, { offerCandidates: updatedCandidates });
} else {
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) => {
try {
const origin = getOrigin(c);
const body = await c.req.json();
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);
}
const offer = await storage.getOffer(code, origin);
const offer = await storage.getOffer(code);
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') {

View File

@@ -27,7 +27,6 @@ export class D1Storage implements Storage {
await this.db.exec(`
CREATE TABLE IF NOT EXISTS offers (
code TEXT PRIMARY KEY,
origin TEXT NOT NULL,
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
offer TEXT NOT NULL,
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_origin ON offers(origin);
`);
}
async createOffer(
origin: string,
peerId: string,
offer: string,
expiresAt: number,
@@ -64,9 +61,9 @@ export class D1Storage implements Storage {
try {
await this.db.prepare(`
INSERT INTO offers (code, origin, peer_id, offer, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
`).bind(code, origin, peerId, offer, Date.now(), expiresAt).run();
INSERT INTO offers (code, peer_id, offer, created_at, expires_at)
VALUES (?, ?, ?, ?, ?)
`).bind(code, peerId, offer, Date.now(), expiresAt).run();
break;
} catch (err: any) {
@@ -85,12 +82,12 @@ export class D1Storage implements Storage {
return code;
}
async getOffer(code: string, origin: string): Promise<Offer | null> {
async getOffer(code: string): Promise<Offer | null> {
try {
const result = await this.db.prepare(`
SELECT * FROM offers
WHERE code = ? AND origin = ? AND expires_at > ?
`).bind(code, origin, Date.now()).first();
WHERE code = ? AND expires_at > ?
`).bind(code, Date.now()).first();
if (!result) {
return null;
@@ -100,7 +97,6 @@ export class D1Storage implements Storage {
return {
code: row.code,
origin: row.origin,
peerId: row.peer_id,
offer: row.offer,
answer: row.answer || undefined,
@@ -115,12 +111,12 @@ export class D1Storage implements Storage {
}
}
async updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void> {
// Verify offer exists and origin matches
const current = await this.getOffer(code, origin);
async updateOffer(code: string, update: Partial<Offer>): Promise<void> {
// Verify offer exists
const current = await this.getOffer(code);
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
@@ -147,13 +143,13 @@ export class D1Storage implements Storage {
}
// Add WHERE clause values
values.push(code, origin);
values.push(code);
// D1 provides strong consistency, so this update is atomic and immediately visible
const query = `
UPDATE offers
SET ${updates.join(', ')}
WHERE code = ? AND origin = ?
WHERE code = ?
`;
await this.db.prepare(query).bind(...values).run();

View File

@@ -26,7 +26,6 @@ export class SQLiteStorage implements Storage {
this.db.exec(`
CREATE TABLE IF NOT EXISTS offers (
code TEXT PRIMARY KEY,
origin TEXT NOT NULL,
peer_id TEXT NOT NULL CHECK(length(peer_id) <= 1024),
offer TEXT NOT NULL,
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_origin ON offers(origin);
`);
}
@@ -60,7 +58,7 @@ export class SQLiteStorage implements Storage {
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
if (peerId.length > 1024) {
throw new Error('PeerId string must be 1024 characters or less');
@@ -81,11 +79,11 @@ export class SQLiteStorage implements Storage {
try {
const stmt = this.db.prepare(`
INSERT INTO offers (code, origin, peer_id, offer, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO offers (code, peer_id, offer, created_at, expires_at)
VALUES (?, ?, ?, ?, ?)
`);
stmt.run(code, origin, peerId, offer, Date.now(), expiresAt);
stmt.run(code, peerId, offer, Date.now(), expiresAt);
break;
} catch (err: any) {
// If unique constraint failed with custom code, throw error
@@ -103,12 +101,12 @@ export class SQLiteStorage implements Storage {
return code;
}
async getOffer(code: string, origin: string): Promise<Offer | null> {
async getOffer(code: string): Promise<Offer | null> {
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) {
return null;
@@ -116,7 +114,6 @@ export class SQLiteStorage implements Storage {
return {
code: row.code,
origin: row.origin,
peerId: row.peer_id,
offer: row.offer,
answer: row.answer || undefined,
@@ -127,11 +124,11 @@ export class SQLiteStorage implements Storage {
};
}
async updateOffer(code: string, origin: string, update: Partial<Offer>): Promise<void> {
const current = await this.getOffer(code, origin);
async updateOffer(code: string, update: Partial<Offer>): Promise<void> {
const current = await this.getOffer(code);
if (!current) {
throw new Error('Offer not found or origin mismatch');
throw new Error('Offer not found');
}
const updates: string[] = [];
@@ -157,10 +154,9 @@ export class SQLiteStorage implements Storage {
}
values.push(code);
values.push(origin);
const stmt = this.db.prepare(`
UPDATE offers SET ${updates.join(', ')} WHERE code = ? AND origin = ?
UPDATE offers SET ${updates.join(', ')} WHERE code = ?
`);
stmt.run(...values);

View File

@@ -3,7 +3,6 @@
*/
export interface Offer {
code: string;
origin: string;
peerId: string;
offer: string;
answer?: string;
@@ -20,30 +19,27 @@ export interface Offer {
export interface Storage {
/**
* Creates a new offer
* @param origin The Origin header from the request
* @param peerId Peer identifier string (max 1024 chars)
* @param offer The WebRTC SDP offer message
* @param expiresAt Unix timestamp when the offer should expire
* @param customCode Optional custom code (if not provided, generates UUID)
* @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
* @param code The offer code
* @param origin The Origin header from the request (for validation)
* @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
* @param code The offer code
* @param origin The Origin header from the request (for validation)
* @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