@xtr-dev/rondevu-server 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,17 @@ Health check endpoint with version
53
53
  #### `POST /register`
54
54
  Register a new peer and receive credentials (peerId + secret)
55
55
 
56
+ **Request (optional):**
57
+ ```json
58
+ {
59
+ "peerId": "my-custom-peer-id"
60
+ }
61
+ ```
62
+
63
+ **Notes:**
64
+ - `peerId` (optional): Custom peer ID (1-128 characters). If not provided, a random ID will be generated.
65
+ - Returns 409 Conflict if the custom peer ID is already in use.
66
+
56
67
  **Response:**
57
68
  ```json
58
69
  {
package/dist/index.js CHANGED
@@ -239,7 +239,24 @@ function createApp(storage, config) {
239
239
  });
240
240
  app.post("/register", async (c) => {
241
241
  try {
242
- const peerId = generatePeerId();
242
+ let peerId;
243
+ const body = await c.req.json().catch(() => ({}));
244
+ const customPeerId = body.peerId;
245
+ if (customPeerId !== void 0) {
246
+ if (typeof customPeerId !== "string" || customPeerId.length === 0) {
247
+ return c.json({ error: "Peer ID must be a non-empty string" }, 400);
248
+ }
249
+ if (customPeerId.length > 128) {
250
+ return c.json({ error: "Peer ID must be 128 characters or less" }, 400);
251
+ }
252
+ const existingOffers = await storage.getOffersByPeerId(customPeerId);
253
+ if (existingOffers.length > 0) {
254
+ return c.json({ error: "Peer ID is already in use" }, 409);
255
+ }
256
+ peerId = customPeerId;
257
+ } else {
258
+ peerId = generatePeerId();
259
+ }
243
260
  const secret = await encryptPeerId(peerId, config.authSecret);
244
261
  return c.json({
245
262
  peerId,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts", "../src/app.ts", "../src/crypto.ts", "../src/middleware/auth.ts", "../src/bloom.ts", "../src/config.ts", "../src/storage/sqlite.ts", "../src/storage/hash-id.ts"],
4
- "sourcesContent": ["import { serve } from '@hono/node-server';\nimport { createApp } from './app.ts';\nimport { loadConfig } from './config.ts';\nimport { SQLiteStorage } from './storage/sqlite.ts';\nimport { Storage } from './storage/types.ts';\n\n/**\n * Main entry point for the standalone Node.js server\n */\nasync function main() {\n const config = loadConfig();\n\n console.log('Starting Rondevu server...');\n console.log('Configuration:', {\n port: config.port,\n storageType: config.storageType,\n storagePath: config.storagePath,\n offerDefaultTtl: `${config.offerDefaultTtl}ms`,\n offerMaxTtl: `${config.offerMaxTtl}ms`,\n offerMinTtl: `${config.offerMinTtl}ms`,\n cleanupInterval: `${config.cleanupInterval}ms`,\n maxOffersPerRequest: config.maxOffersPerRequest,\n maxTopicsPerOffer: config.maxTopicsPerOffer,\n corsOrigins: config.corsOrigins,\n version: config.version,\n });\n\n let storage: Storage;\n\n if (config.storageType === 'sqlite') {\n storage = new SQLiteStorage(config.storagePath);\n console.log('Using SQLite storage');\n } else {\n throw new Error('Unsupported storage type');\n }\n\n // Start periodic cleanup of expired offers\n const cleanupInterval = setInterval(async () => {\n try {\n const now = Date.now();\n const deleted = await storage.deleteExpiredOffers(now);\n if (deleted > 0) {\n console.log(`Cleanup: Deleted ${deleted} expired offer(s)`);\n }\n } catch (err) {\n console.error('Cleanup error:', err);\n }\n }, config.cleanupInterval);\n\n const app = createApp(storage, config);\n\n const server = serve({\n fetch: app.fetch,\n port: config.port,\n });\n\n console.log(`Server running on http://localhost:${config.port}`);\n console.log('Ready to accept connections');\n\n // Graceful shutdown handler\n const shutdown = async () => {\n console.log('\\nShutting down gracefully...');\n clearInterval(cleanupInterval);\n await storage.close();\n process.exit(0);\n };\n\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n\nmain().catch((err) => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n", "import { Hono } from 'hono';\nimport { cors } from 'hono/cors';\nimport { Storage } from './storage/types.ts';\nimport { Config } from './config.ts';\nimport { createAuthMiddleware, getAuthenticatedPeerId } from './middleware/auth.ts';\nimport { generatePeerId, encryptPeerId } from './crypto.ts';\nimport { parseBloomFilter } from './bloom.ts';\nimport type { Context } from 'hono';\n\n/**\n * Creates the Hono application with topic-based WebRTC signaling endpoints\n */\nexport function createApp(storage: Storage, config: Config) {\n const app = new Hono();\n\n // Create auth middleware\n const authMiddleware = createAuthMiddleware(config.authSecret);\n\n // Enable CORS with dynamic origin handling\n app.use('/*', cors({\n origin: (origin) => {\n // If no origin restrictions (wildcard), allow any origin\n if (config.corsOrigins.length === 1 && config.corsOrigins[0] === '*') {\n return origin;\n }\n // Otherwise check if origin is in allowed list\n if (config.corsOrigins.includes(origin)) {\n return origin;\n }\n // Default to first allowed origin\n return config.corsOrigins[0];\n },\n allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Origin', 'Authorization'],\n exposeHeaders: ['Content-Type'],\n maxAge: 600,\n credentials: true,\n }));\n\n /**\n * GET /\n * Returns server version information\n */\n app.get('/', (c) => {\n return c.json({\n version: config.version,\n name: 'Rondevu',\n description: 'Topic-based peer discovery and signaling server'\n });\n });\n\n /**\n * GET /health\n * Health check endpoint with version\n */\n app.get('/health', (c) => {\n return c.json({\n status: 'ok',\n timestamp: Date.now(),\n version: config.version\n });\n });\n\n /**\n * POST /register\n * Register a new peer and receive credentials\n */\n app.post('/register', async (c) => {\n try {\n // Generate new peer ID\n const peerId = generatePeerId();\n\n // Encrypt peer ID with server secret (async operation)\n const secret = await encryptPeerId(peerId, config.authSecret);\n\n return c.json({\n peerId,\n secret\n }, 200);\n } catch (err) {\n console.error('Error registering peer:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * POST /offers\n * Creates one or more offers with topics\n * Requires authentication\n */\n app.post('/offers', authMiddleware, async (c) => {\n try {\n const body = await c.req.json();\n const { offers } = body;\n\n if (!Array.isArray(offers) || offers.length === 0) {\n return c.json({ error: 'Missing or invalid required parameter: offers (must be non-empty array)' }, 400);\n }\n\n if (offers.length > config.maxOffersPerRequest) {\n return c.json({ error: `Too many offers. Maximum ${config.maxOffersPerRequest} per request` }, 400);\n }\n\n const peerId = getAuthenticatedPeerId(c);\n\n // Validate and prepare offers\n const offerRequests = [];\n for (const offer of offers) {\n // Validate SDP\n if (!offer.sdp || typeof offer.sdp !== 'string') {\n return c.json({ error: 'Each offer must have an sdp field' }, 400);\n }\n\n if (offer.sdp.length > 65536) {\n return c.json({ error: 'SDP must be 64KB or less' }, 400);\n }\n\n // Validate secret if provided\n if (offer.secret !== undefined) {\n if (typeof offer.secret !== 'string') {\n return c.json({ error: 'Secret must be a string' }, 400);\n }\n if (offer.secret.length > 128) {\n return c.json({ error: 'Secret must be 128 characters or less' }, 400);\n }\n }\n\n // Validate topics\n if (!Array.isArray(offer.topics) || offer.topics.length === 0) {\n return c.json({ error: 'Each offer must have a non-empty topics array' }, 400);\n }\n\n if (offer.topics.length > config.maxTopicsPerOffer) {\n return c.json({ error: `Too many topics. Maximum ${config.maxTopicsPerOffer} per offer` }, 400);\n }\n\n for (const topic of offer.topics) {\n if (typeof topic !== 'string' || topic.length === 0 || topic.length > 256) {\n return c.json({ error: 'Each topic must be a string between 1 and 256 characters' }, 400);\n }\n }\n\n // Validate and clamp TTL\n let ttl = offer.ttl || config.offerDefaultTtl;\n if (ttl < config.offerMinTtl) {\n ttl = config.offerMinTtl;\n }\n if (ttl > config.offerMaxTtl) {\n ttl = config.offerMaxTtl;\n }\n\n offerRequests.push({\n id: offer.id,\n peerId,\n sdp: offer.sdp,\n topics: offer.topics,\n expiresAt: Date.now() + ttl,\n secret: offer.secret,\n });\n }\n\n // Create offers\n const createdOffers = await storage.createOffers(offerRequests);\n\n // Return simplified response\n return c.json({\n offers: createdOffers.map(o => ({\n id: o.id,\n peerId: o.peerId,\n topics: o.topics,\n expiresAt: o.expiresAt\n }))\n }, 200);\n } catch (err) {\n console.error('Error creating offers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/by-topic/:topic\n * Find offers by topic with optional bloom filter exclusion\n * Public endpoint (no auth required)\n */\n app.get('/offers/by-topic/:topic', async (c) => {\n try {\n const topic = c.req.param('topic');\n const bloomParam = c.req.query('bloom');\n const limitParam = c.req.query('limit');\n\n const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;\n\n // Parse bloom filter if provided\n let excludePeerIds: string[] = [];\n if (bloomParam) {\n const bloom = parseBloomFilter(bloomParam);\n if (!bloom) {\n return c.json({ error: 'Invalid bloom filter format' }, 400);\n }\n\n // Get all offers for topic first\n const allOffers = await storage.getOffersByTopic(topic);\n\n // Test each peer ID against bloom filter\n const excludeSet = new Set<string>();\n for (const offer of allOffers) {\n if (bloom.test(offer.peerId)) {\n excludeSet.add(offer.peerId);\n }\n }\n\n excludePeerIds = Array.from(excludeSet);\n }\n\n // Get filtered offers\n let offers = await storage.getOffersByTopic(topic, excludePeerIds.length > 0 ? excludePeerIds : undefined);\n\n // Apply limit\n const total = offers.length;\n offers = offers.slice(0, limit);\n\n return c.json({\n topic,\n offers: offers.map(o => ({\n id: o.id,\n peerId: o.peerId,\n sdp: o.sdp,\n topics: o.topics,\n expiresAt: o.expiresAt,\n lastSeen: o.lastSeen,\n hasSecret: !!o.secret // Indicate if secret is required without exposing it\n })),\n total: bloomParam ? total + excludePeerIds.length : total,\n returned: offers.length\n }, 200);\n } catch (err) {\n console.error('Error fetching offers by topic:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /topics\n * List all topics with active peer counts (paginated)\n * Public endpoint (no auth required)\n * Query params:\n * - limit: Max topics to return (default 50, max 200)\n * - offset: Number of topics to skip (default 0)\n * - startsWith: Filter topics starting with this prefix (optional)\n */\n app.get('/topics', async (c) => {\n try {\n const limitParam = c.req.query('limit');\n const offsetParam = c.req.query('offset');\n const startsWithParam = c.req.query('startsWith');\n\n const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;\n const offset = offsetParam ? parseInt(offsetParam, 10) : 0;\n const startsWith = startsWithParam || undefined;\n\n const result = await storage.getTopics(limit, offset, startsWith);\n\n return c.json({\n topics: result.topics,\n total: result.total,\n limit,\n offset,\n ...(startsWith && { startsWith })\n }, 200);\n } catch (err) {\n console.error('Error fetching topics:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /peers/:peerId/offers\n * View all offers from a specific peer\n * Public endpoint\n */\n app.get('/peers/:peerId/offers', async (c) => {\n try {\n const peerId = c.req.param('peerId');\n const offers = await storage.getOffersByPeerId(peerId);\n\n // Collect unique topics\n const topicsSet = new Set<string>();\n offers.forEach(o => o.topics.forEach(t => topicsSet.add(t)));\n\n return c.json({\n peerId,\n offers: offers.map(o => ({\n id: o.id,\n sdp: o.sdp,\n topics: o.topics,\n expiresAt: o.expiresAt,\n lastSeen: o.lastSeen,\n hasSecret: !!o.secret // Indicate if secret is required without exposing it\n })),\n topics: Array.from(topicsSet)\n }, 200);\n } catch (err) {\n console.error('Error fetching peer offers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/mine\n * List all offers owned by authenticated peer\n * Requires authentication\n */\n app.get('/offers/mine', authMiddleware, async (c) => {\n try {\n const peerId = getAuthenticatedPeerId(c);\n const offers = await storage.getOffersByPeerId(peerId);\n\n return c.json({\n peerId,\n offers: offers.map(o => ({\n id: o.id,\n sdp: o.sdp,\n topics: o.topics,\n createdAt: o.createdAt,\n expiresAt: o.expiresAt,\n lastSeen: o.lastSeen,\n secret: o.secret, // Owner can see the secret\n answererPeerId: o.answererPeerId,\n answeredAt: o.answeredAt\n }))\n }, 200);\n } catch (err) {\n console.error('Error fetching own offers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * DELETE /offers/:offerId\n * Delete a specific offer\n * Requires authentication and ownership\n */\n app.delete('/offers/:offerId', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n\n const deleted = await storage.deleteOffer(offerId, peerId);\n\n if (!deleted) {\n return c.json({ error: 'Offer not found or not authorized' }, 404);\n }\n\n return c.json({ deleted: true }, 200);\n } catch (err) {\n console.error('Error deleting offer:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * POST /offers/:offerId/answer\n * Answer a specific offer (locks it to answerer)\n * Requires authentication\n */\n app.post('/offers/:offerId/answer', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n const body = await c.req.json();\n const { sdp, secret } = body;\n\n if (!sdp || typeof sdp !== 'string') {\n return c.json({ error: 'Missing or invalid required parameter: sdp' }, 400);\n }\n\n if (sdp.length > 65536) {\n return c.json({ error: 'SDP must be 64KB or less' }, 400);\n }\n\n // Validate secret if provided\n if (secret !== undefined && typeof secret !== 'string') {\n return c.json({ error: 'Secret must be a string' }, 400);\n }\n\n const result = await storage.answerOffer(offerId, peerId, sdp, secret);\n\n if (!result.success) {\n return c.json({ error: result.error }, 400);\n }\n\n return c.json({\n offerId,\n answererId: peerId,\n answeredAt: Date.now()\n }, 200);\n } catch (err) {\n console.error('Error answering offer:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/answers\n * Poll for answers to all of authenticated peer's offers\n * Requires authentication (offerer)\n */\n app.get('/offers/answers', authMiddleware, async (c) => {\n try {\n const peerId = getAuthenticatedPeerId(c);\n const offers = await storage.getAnsweredOffers(peerId);\n\n return c.json({\n answers: offers.map(o => ({\n offerId: o.id,\n answererId: o.answererPeerId,\n sdp: o.answerSdp,\n answeredAt: o.answeredAt,\n topics: o.topics\n }))\n }, 200);\n } catch (err) {\n console.error('Error fetching answers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * POST /offers/:offerId/ice-candidates\n * Post ICE candidates for an offer\n * Requires authentication (must be offerer or answerer)\n */\n app.post('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n const body = await c.req.json();\n const { candidates } = body;\n\n if (!Array.isArray(candidates) || candidates.length === 0) {\n return c.json({ error: 'Missing or invalid required parameter: candidates (must be non-empty array)' }, 400);\n }\n\n // Verify offer exists and caller is offerer or answerer\n const offer = await storage.getOfferById(offerId);\n if (!offer) {\n return c.json({ error: 'Offer not found or expired' }, 404);\n }\n\n let role: 'offerer' | 'answerer';\n if (offer.peerId === peerId) {\n role = 'offerer';\n } else if (offer.answererPeerId === peerId) {\n role = 'answerer';\n } else {\n return c.json({ error: 'Not authorized to post ICE candidates for this offer' }, 403);\n }\n\n const added = await storage.addIceCandidates(offerId, peerId, role, candidates);\n\n return c.json({\n offerId,\n candidatesAdded: added\n }, 200);\n } catch (err) {\n console.error('Error adding ICE candidates:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/:offerId/ice-candidates\n * Poll for ICE candidates from the other peer\n * Requires authentication (must be offerer or answerer)\n */\n app.get('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n const sinceParam = c.req.query('since');\n\n const since = sinceParam ? parseInt(sinceParam, 10) : undefined;\n\n // Verify offer exists and caller is offerer or answerer\n const offer = await storage.getOfferById(offerId);\n if (!offer) {\n return c.json({ error: 'Offer not found or expired' }, 404);\n }\n\n let targetRole: 'offerer' | 'answerer';\n if (offer.peerId === peerId) {\n // Offerer wants answerer's candidates\n targetRole = 'answerer';\n console.log(`[ICE GET] Offerer ${peerId} requesting answerer ICE candidates for offer ${offerId}, since=${since}, answererPeerId=${offer.answererPeerId}`);\n } else if (offer.answererPeerId === peerId) {\n // Answerer wants offerer's candidates\n targetRole = 'offerer';\n console.log(`[ICE GET] Answerer ${peerId} requesting offerer ICE candidates for offer ${offerId}, since=${since}, offererPeerId=${offer.peerId}`);\n } else {\n return c.json({ error: 'Not authorized to view ICE candidates for this offer' }, 403);\n }\n\n const candidates = await storage.getIceCandidates(offerId, targetRole, since);\n console.log(`[ICE GET] Found ${candidates.length} candidates for offer ${offerId}, targetRole=${targetRole}, since=${since}`);\n\n return c.json({\n offerId,\n candidates: candidates.map(c => ({\n candidate: c.candidate,\n peerId: c.peerId,\n role: c.role,\n createdAt: c.createdAt\n }))\n }, 200);\n } catch (err) {\n console.error('Error fetching ICE candidates:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n return app;\n}\n", "/**\n * Crypto utilities for stateless peer authentication\n * Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers\n */\n\nconst ALGORITHM = 'AES-GCM';\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst KEY_LENGTH = 32; // 256 bits\n\n/**\n * Generates a random peer ID (16 bytes = 32 hex chars)\n */\nexport function generatePeerId(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Generates a random secret key for encryption (32 bytes = 64 hex chars)\n */\nexport function generateSecretKey(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));\n return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Convert hex string to Uint8Array\n */\nfunction hexToBytes(hex: string): Uint8Array {\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < hex.length; i += 2) {\n bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Convert Uint8Array to base64 string\n */\nfunction bytesToBase64(bytes: Uint8Array): string {\n const binString = Array.from(bytes, (byte) =>\n String.fromCodePoint(byte)\n ).join('');\n return btoa(binString);\n}\n\n/**\n * Convert base64 string to Uint8Array\n */\nfunction base64ToBytes(base64: string): Uint8Array {\n const binString = atob(base64);\n return Uint8Array.from(binString, (char) => char.codePointAt(0)!);\n}\n\n/**\n * Encrypts a peer ID using the server secret key\n * Returns base64-encoded encrypted data (IV + ciphertext)\n */\nexport async function encryptPeerId(peerId: string, secretKeyHex: string): Promise<string> {\n const keyBytes = hexToBytes(secretKeyHex);\n\n if (keyBytes.length !== KEY_LENGTH) {\n throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);\n }\n\n // Import key\n const key = await crypto.subtle.importKey(\n 'raw',\n keyBytes,\n { name: ALGORITHM, length: 256 },\n false,\n ['encrypt']\n );\n\n // Generate random IV\n const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));\n\n // Encrypt peer ID\n const encoder = new TextEncoder();\n const data = encoder.encode(peerId);\n\n const encrypted = await crypto.subtle.encrypt(\n { name: ALGORITHM, iv },\n key,\n data\n );\n\n // Combine IV + ciphertext and encode as base64\n const combined = new Uint8Array(iv.length + encrypted.byteLength);\n combined.set(iv, 0);\n combined.set(new Uint8Array(encrypted), iv.length);\n\n return bytesToBase64(combined);\n}\n\n/**\n * Decrypts an encrypted peer ID secret\n * Returns the plaintext peer ID or throws if decryption fails\n */\nexport async function decryptPeerId(encryptedSecret: string, secretKeyHex: string): Promise<string> {\n try {\n const keyBytes = hexToBytes(secretKeyHex);\n\n if (keyBytes.length !== KEY_LENGTH) {\n throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);\n }\n\n // Decode base64\n const combined = base64ToBytes(encryptedSecret);\n\n // Extract IV and ciphertext\n const iv = combined.slice(0, IV_LENGTH);\n const ciphertext = combined.slice(IV_LENGTH);\n\n // Import key\n const key = await crypto.subtle.importKey(\n 'raw',\n keyBytes,\n { name: ALGORITHM, length: 256 },\n false,\n ['decrypt']\n );\n\n // Decrypt\n const decrypted = await crypto.subtle.decrypt(\n { name: ALGORITHM, iv },\n key,\n ciphertext\n );\n\n const decoder = new TextDecoder();\n return decoder.decode(decrypted);\n } catch (err) {\n throw new Error('Failed to decrypt peer ID: invalid secret or secret key');\n }\n}\n\n/**\n * Validates that a peer ID and secret match\n * Returns true if valid, false otherwise\n */\nexport async function validateCredentials(peerId: string, encryptedSecret: string, secretKey: string): Promise<boolean> {\n try {\n const decryptedPeerId = await decryptPeerId(encryptedSecret, secretKey);\n return decryptedPeerId === peerId;\n } catch {\n return false;\n }\n}\n", "import { Context, Next } from 'hono';\nimport { validateCredentials } from '../crypto.ts';\n\n/**\n * Authentication middleware for Rondevu\n * Validates Bearer token in format: {peerId}:{encryptedSecret}\n */\nexport function createAuthMiddleware(authSecret: string) {\n return async (c: Context, next: Next) => {\n const authHeader = c.req.header('Authorization');\n\n if (!authHeader) {\n return c.json({ error: 'Missing Authorization header' }, 401);\n }\n\n // Expect format: Bearer {peerId}:{secret}\n const parts = authHeader.split(' ');\n if (parts.length !== 2 || parts[0] !== 'Bearer') {\n return c.json({ error: 'Invalid Authorization header format. Expected: Bearer {peerId}:{secret}' }, 401);\n }\n\n const credentials = parts[1].split(':');\n if (credentials.length !== 2) {\n return c.json({ error: 'Invalid credentials format. Expected: {peerId}:{secret}' }, 401);\n }\n\n const [peerId, encryptedSecret] = credentials;\n\n // Validate credentials (async operation)\n const isValid = await validateCredentials(peerId, encryptedSecret, authSecret);\n if (!isValid) {\n return c.json({ error: 'Invalid credentials' }, 401);\n }\n\n // Attach peer ID to context for use in handlers\n c.set('peerId', peerId);\n\n await next();\n };\n}\n\n/**\n * Helper to get authenticated peer ID from context\n */\nexport function getAuthenticatedPeerId(c: Context): string {\n const peerId = c.get('peerId');\n if (!peerId) {\n throw new Error('No authenticated peer ID in context');\n }\n return peerId;\n}\n", "/**\n * Bloom filter utility for testing if peer IDs might be in a set\n * Used to filter out known peers from discovery results\n */\n\nexport class BloomFilter {\n private bits: Uint8Array;\n private size: number;\n private numHashes: number;\n\n /**\n * Creates a bloom filter from a base64 encoded bit array\n */\n constructor(base64Data: string, numHashes: number = 3) {\n // Decode base64 to Uint8Array (works in both Node.js and Workers)\n const binaryString = atob(base64Data);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n this.bits = bytes;\n this.size = this.bits.length * 8;\n this.numHashes = numHashes;\n }\n\n /**\n * Test if a peer ID might be in the filter\n * Returns true if possibly in set, false if definitely not in set\n */\n test(peerId: string): boolean {\n for (let i = 0; i < this.numHashes; i++) {\n const hash = this.hash(peerId, i);\n const index = hash % this.size;\n const byteIndex = Math.floor(index / 8);\n const bitIndex = index % 8;\n\n if (!(this.bits[byteIndex] & (1 << bitIndex))) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Simple hash function (FNV-1a variant)\n */\n private hash(str: string, seed: number): number {\n let hash = 2166136261 ^ seed;\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n }\n return hash >>> 0;\n }\n}\n\n/**\n * Helper to parse bloom filter from base64 string\n */\nexport function parseBloomFilter(base64: string): BloomFilter | null {\n try {\n return new BloomFilter(base64);\n } catch {\n return null;\n }\n}\n", "import { generateSecretKey } from './crypto.ts';\n\n/**\n * Application configuration\n * Reads from environment variables with sensible defaults\n */\nexport interface Config {\n port: number;\n storageType: 'sqlite' | 'memory';\n storagePath: string;\n corsOrigins: string[];\n version: string;\n authSecret: string;\n offerDefaultTtl: number;\n offerMaxTtl: number;\n offerMinTtl: number;\n cleanupInterval: number;\n maxOffersPerRequest: number;\n maxTopicsPerOffer: number;\n}\n\n/**\n * Loads configuration from environment variables\n */\nexport function loadConfig(): Config {\n // Generate or load auth secret\n let authSecret = process.env.AUTH_SECRET;\n if (!authSecret) {\n authSecret = generateSecretKey();\n console.warn('WARNING: No AUTH_SECRET provided. Generated temporary secret:', authSecret);\n console.warn('All peer credentials will be invalidated on server restart.');\n console.warn('Set AUTH_SECRET environment variable to persist credentials across restarts.');\n }\n\n return {\n port: parseInt(process.env.PORT || '3000', 10),\n storageType: (process.env.STORAGE_TYPE || 'sqlite') as 'sqlite' | 'memory',\n storagePath: process.env.STORAGE_PATH || ':memory:',\n corsOrigins: process.env.CORS_ORIGINS\n ? process.env.CORS_ORIGINS.split(',').map(o => o.trim())\n : ['*'],\n version: process.env.VERSION || 'unknown',\n authSecret,\n offerDefaultTtl: parseInt(process.env.OFFER_DEFAULT_TTL || '60000', 10),\n offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || '86400000', 10),\n offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || '60000', 10),\n cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || '60000', 10),\n maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10),\n maxTopicsPerOffer: parseInt(process.env.MAX_TOPICS_PER_OFFER || '50', 10),\n };\n}\n", "import Database from 'better-sqlite3';\nimport { Storage, Offer, IceCandidate, CreateOfferRequest, TopicInfo } from './types.ts';\nimport { generateOfferHash } from './hash-id.ts';\n\n/**\n * SQLite storage adapter for topic-based offer management\n * Supports both file-based and in-memory databases\n */\nexport class SQLiteStorage implements Storage {\n private db: Database.Database;\n\n /**\n * Creates a new SQLite storage instance\n * @param path Path to SQLite database file, or ':memory:' for in-memory database\n */\n constructor(path: string = ':memory:') {\n this.db = new Database(path);\n this.initializeDatabase();\n }\n\n /**\n * Initializes database schema with new topic-based structure\n */\n private initializeDatabase(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS offers (\n id TEXT PRIMARY KEY,\n peer_id TEXT NOT NULL,\n sdp TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n last_seen INTEGER NOT NULL,\n secret TEXT,\n answerer_peer_id TEXT,\n answer_sdp TEXT,\n answered_at INTEGER\n );\n\n CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);\n CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);\n CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);\n CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);\n\n CREATE TABLE IF NOT EXISTS offer_topics (\n offer_id TEXT NOT NULL,\n topic TEXT NOT NULL,\n PRIMARY KEY (offer_id, topic),\n FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_topics_topic ON offer_topics(topic);\n CREATE INDEX IF NOT EXISTS idx_topics_offer ON offer_topics(offer_id);\n\n CREATE TABLE IF NOT EXISTS ice_candidates (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n offer_id TEXT NOT NULL,\n peer_id TEXT NOT NULL,\n role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),\n candidate TEXT NOT NULL, -- JSON: RTCIceCandidateInit object\n created_at INTEGER NOT NULL,\n FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);\n CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);\n CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);\n `);\n\n // Enable foreign keys\n this.db.pragma('foreign_keys = ON');\n }\n\n async createOffers(offers: CreateOfferRequest[]): Promise<Offer[]> {\n const created: Offer[] = [];\n\n // Generate hash-based IDs for all offers first\n const offersWithIds = await Promise.all(\n offers.map(async (offer) => ({\n ...offer,\n id: offer.id || await generateOfferHash(offer.sdp, offer.topics),\n }))\n );\n\n // Use transaction for atomic creation\n const transaction = this.db.transaction((offersWithIds: (CreateOfferRequest & { id: string })[]) => {\n const offerStmt = this.db.prepare(`\n INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n const topicStmt = this.db.prepare(`\n INSERT INTO offer_topics (offer_id, topic)\n VALUES (?, ?)\n `);\n\n for (const offer of offersWithIds) {\n const now = Date.now();\n\n // Insert offer\n offerStmt.run(\n offer.id,\n offer.peerId,\n offer.sdp,\n now,\n offer.expiresAt,\n now,\n offer.secret || null\n );\n\n // Insert topics\n for (const topic of offer.topics) {\n topicStmt.run(offer.id, topic);\n }\n\n created.push({\n id: offer.id,\n peerId: offer.peerId,\n sdp: offer.sdp,\n topics: offer.topics,\n createdAt: now,\n expiresAt: offer.expiresAt,\n lastSeen: now,\n secret: offer.secret,\n });\n }\n });\n\n transaction(offersWithIds);\n return created;\n }\n\n async getOffersByTopic(topic: string, excludePeerIds?: string[]): Promise<Offer[]> {\n let query = `\n SELECT DISTINCT o.*\n FROM offers o\n INNER JOIN offer_topics ot ON o.id = ot.offer_id\n WHERE ot.topic = ? AND o.expires_at > ?\n `;\n\n const params: any[] = [topic, Date.now()];\n\n if (excludePeerIds && excludePeerIds.length > 0) {\n const placeholders = excludePeerIds.map(() => '?').join(',');\n query += ` AND o.peer_id NOT IN (${placeholders})`;\n params.push(...excludePeerIds);\n }\n\n query += ' ORDER BY o.last_seen DESC';\n\n const stmt = this.db.prepare(query);\n const rows = stmt.all(...params) as any[];\n\n return Promise.all(rows.map(row => this.rowToOffer(row)));\n }\n\n async getOffersByPeerId(peerId: string): Promise<Offer[]> {\n const stmt = this.db.prepare(`\n SELECT * FROM offers\n WHERE peer_id = ? AND expires_at > ?\n ORDER BY last_seen DESC\n `);\n\n const rows = stmt.all(peerId, Date.now()) as any[];\n return Promise.all(rows.map(row => this.rowToOffer(row)));\n }\n\n async getOfferById(offerId: string): Promise<Offer | null> {\n const stmt = this.db.prepare(`\n SELECT * FROM offers\n WHERE id = ? AND expires_at > ?\n `);\n\n const row = stmt.get(offerId, Date.now()) as any;\n\n if (!row) {\n return null;\n }\n\n return this.rowToOffer(row);\n }\n\n async deleteOffer(offerId: string, ownerPeerId: string): Promise<boolean> {\n const stmt = this.db.prepare(`\n DELETE FROM offers\n WHERE id = ? AND peer_id = ?\n `);\n\n const result = stmt.run(offerId, ownerPeerId);\n return result.changes > 0;\n }\n\n async deleteExpiredOffers(now: number): Promise<number> {\n const stmt = this.db.prepare('DELETE FROM offers WHERE expires_at < ?');\n const result = stmt.run(now);\n return result.changes;\n }\n\n async answerOffer(\n offerId: string,\n answererPeerId: string,\n answerSdp: string,\n secret?: string\n ): Promise<{ success: boolean; error?: string }> {\n // Check if offer exists and is not expired\n const offer = await this.getOfferById(offerId);\n\n if (!offer) {\n return {\n success: false,\n error: 'Offer not found or expired'\n };\n }\n\n // Verify secret if offer is protected\n if (offer.secret && offer.secret !== secret) {\n return {\n success: false,\n error: 'Invalid or missing secret'\n };\n }\n\n // Check if offer already has an answerer\n if (offer.answererPeerId) {\n return {\n success: false,\n error: 'Offer already answered'\n };\n }\n\n // Update offer with answer\n const stmt = this.db.prepare(`\n UPDATE offers\n SET answerer_peer_id = ?, answer_sdp = ?, answered_at = ?\n WHERE id = ? AND answerer_peer_id IS NULL\n `);\n\n const result = stmt.run(answererPeerId, answerSdp, Date.now(), offerId);\n\n if (result.changes === 0) {\n return {\n success: false,\n error: 'Offer already answered (race condition)'\n };\n }\n\n return { success: true };\n }\n\n async getAnsweredOffers(offererPeerId: string): Promise<Offer[]> {\n const stmt = this.db.prepare(`\n SELECT * FROM offers\n WHERE peer_id = ? AND answerer_peer_id IS NOT NULL AND expires_at > ?\n ORDER BY answered_at DESC\n `);\n\n const rows = stmt.all(offererPeerId, Date.now()) as any[];\n return Promise.all(rows.map(row => this.rowToOffer(row)));\n }\n\n async addIceCandidates(\n offerId: string,\n peerId: string,\n role: 'offerer' | 'answerer',\n candidates: any[]\n ): Promise<number> {\n const stmt = this.db.prepare(`\n INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)\n VALUES (?, ?, ?, ?, ?)\n `);\n\n const baseTimestamp = Date.now();\n const transaction = this.db.transaction((candidates: any[]) => {\n for (let i = 0; i < candidates.length; i++) {\n stmt.run(\n offerId,\n peerId,\n role,\n JSON.stringify(candidates[i]), // Store full object as JSON\n baseTimestamp + i // Ensure unique timestamps to avoid \"since\" filtering issues\n );\n }\n });\n\n transaction(candidates);\n return candidates.length;\n }\n\n async getIceCandidates(\n offerId: string,\n targetRole: 'offerer' | 'answerer',\n since?: number\n ): Promise<IceCandidate[]> {\n let query = `\n SELECT * FROM ice_candidates\n WHERE offer_id = ? AND role = ?\n `;\n\n const params: any[] = [offerId, targetRole];\n\n if (since !== undefined) {\n query += ' AND created_at > ?';\n params.push(since);\n }\n\n query += ' ORDER BY created_at ASC';\n\n const stmt = this.db.prepare(query);\n const rows = stmt.all(...params) as any[];\n\n return rows.map(row => ({\n id: row.id,\n offerId: row.offer_id,\n peerId: row.peer_id,\n role: row.role,\n candidate: JSON.parse(row.candidate), // Parse JSON back to object\n createdAt: row.created_at,\n }));\n }\n\n async getTopics(limit: number, offset: number, startsWith?: string): Promise<{\n topics: TopicInfo[];\n total: number;\n }> {\n const now = Date.now();\n\n // Build WHERE clause for startsWith filter\n const whereClause = startsWith\n ? 'o.expires_at > ? AND ot.topic LIKE ?'\n : 'o.expires_at > ?';\n\n const startsWithPattern = startsWith ? `${startsWith}%` : null;\n\n // Get total count of topics with active offers\n const countQuery = `\n SELECT COUNT(DISTINCT ot.topic) as count\n FROM offer_topics ot\n INNER JOIN offers o ON ot.offer_id = o.id\n WHERE ${whereClause}\n `;\n\n const countStmt = this.db.prepare(countQuery);\n const countParams = startsWith ? [now, startsWithPattern] : [now];\n const countRow = countStmt.get(...countParams) as any;\n const total = countRow.count;\n\n // Get topics with peer counts (paginated)\n const topicsQuery = `\n SELECT\n ot.topic,\n COUNT(DISTINCT o.peer_id) as active_peers\n FROM offer_topics ot\n INNER JOIN offers o ON ot.offer_id = o.id\n WHERE ${whereClause}\n GROUP BY ot.topic\n ORDER BY active_peers DESC, ot.topic ASC\n LIMIT ? OFFSET ?\n `;\n\n const topicsStmt = this.db.prepare(topicsQuery);\n const topicsParams = startsWith\n ? [now, startsWithPattern, limit, offset]\n : [now, limit, offset];\n const rows = topicsStmt.all(...topicsParams) as any[];\n\n const topics = rows.map(row => ({\n topic: row.topic,\n activePeers: row.active_peers,\n }));\n\n return { topics, total };\n }\n\n async close(): Promise<void> {\n this.db.close();\n }\n\n /**\n * Helper method to convert database row to Offer object with topics\n */\n private async rowToOffer(row: any): Promise<Offer> {\n // Get topics for this offer\n const topicStmt = this.db.prepare(`\n SELECT topic FROM offer_topics WHERE offer_id = ?\n `);\n\n const topicRows = topicStmt.all(row.id) as any[];\n const topics = topicRows.map(t => t.topic);\n\n return {\n id: row.id,\n peerId: row.peer_id,\n sdp: row.sdp,\n topics,\n createdAt: row.created_at,\n expiresAt: row.expires_at,\n lastSeen: row.last_seen,\n secret: row.secret || undefined,\n answererPeerId: row.answerer_peer_id || undefined,\n answerSdp: row.answer_sdp || undefined,\n answeredAt: row.answered_at || undefined,\n };\n }\n}\n", "/**\n * Generates a content-based offer ID using SHA-256 hash\n * Creates deterministic IDs based on offer content (sdp, topics)\n * PeerID is not included as it's inferred from authentication\n * Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers\n *\n * @param sdp - The WebRTC SDP offer\n * @param topics - Array of topic strings\n * @returns SHA-256 hash of the sanitized offer content\n */\nexport async function generateOfferHash(\n sdp: string,\n topics: string[]\n): Promise<string> {\n // Sanitize and normalize the offer content\n // Only include core offer content (not peerId - that's inferred from auth)\n const sanitizedOffer = {\n sdp,\n topics: [...topics].sort(), // Sort topics for consistency\n };\n\n // Create non-prettified JSON string\n const jsonString = JSON.stringify(sanitizedOffer);\n\n // Convert string to Uint8Array for hashing\n const encoder = new TextEncoder();\n const data = encoder.encode(jsonString);\n\n // Generate SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert hash to hex string\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n\n return hashHex;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { serve } from '@hono/node-server';\nimport { createApp } from './app.ts';\nimport { loadConfig } from './config.ts';\nimport { SQLiteStorage } from './storage/sqlite.ts';\nimport { Storage } from './storage/types.ts';\n\n/**\n * Main entry point for the standalone Node.js server\n */\nasync function main() {\n const config = loadConfig();\n\n console.log('Starting Rondevu server...');\n console.log('Configuration:', {\n port: config.port,\n storageType: config.storageType,\n storagePath: config.storagePath,\n offerDefaultTtl: `${config.offerDefaultTtl}ms`,\n offerMaxTtl: `${config.offerMaxTtl}ms`,\n offerMinTtl: `${config.offerMinTtl}ms`,\n cleanupInterval: `${config.cleanupInterval}ms`,\n maxOffersPerRequest: config.maxOffersPerRequest,\n maxTopicsPerOffer: config.maxTopicsPerOffer,\n corsOrigins: config.corsOrigins,\n version: config.version,\n });\n\n let storage: Storage;\n\n if (config.storageType === 'sqlite') {\n storage = new SQLiteStorage(config.storagePath);\n console.log('Using SQLite storage');\n } else {\n throw new Error('Unsupported storage type');\n }\n\n // Start periodic cleanup of expired offers\n const cleanupInterval = setInterval(async () => {\n try {\n const now = Date.now();\n const deleted = await storage.deleteExpiredOffers(now);\n if (deleted > 0) {\n console.log(`Cleanup: Deleted ${deleted} expired offer(s)`);\n }\n } catch (err) {\n console.error('Cleanup error:', err);\n }\n }, config.cleanupInterval);\n\n const app = createApp(storage, config);\n\n const server = serve({\n fetch: app.fetch,\n port: config.port,\n });\n\n console.log(`Server running on http://localhost:${config.port}`);\n console.log('Ready to accept connections');\n\n // Graceful shutdown handler\n const shutdown = async () => {\n console.log('\\nShutting down gracefully...');\n clearInterval(cleanupInterval);\n await storage.close();\n process.exit(0);\n };\n\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n\nmain().catch((err) => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n", "import { Hono } from 'hono';\nimport { cors } from 'hono/cors';\nimport { Storage } from './storage/types.ts';\nimport { Config } from './config.ts';\nimport { createAuthMiddleware, getAuthenticatedPeerId } from './middleware/auth.ts';\nimport { generatePeerId, encryptPeerId } from './crypto.ts';\nimport { parseBloomFilter } from './bloom.ts';\nimport type { Context } from 'hono';\n\n/**\n * Creates the Hono application with topic-based WebRTC signaling endpoints\n */\nexport function createApp(storage: Storage, config: Config) {\n const app = new Hono();\n\n // Create auth middleware\n const authMiddleware = createAuthMiddleware(config.authSecret);\n\n // Enable CORS with dynamic origin handling\n app.use('/*', cors({\n origin: (origin) => {\n // If no origin restrictions (wildcard), allow any origin\n if (config.corsOrigins.length === 1 && config.corsOrigins[0] === '*') {\n return origin;\n }\n // Otherwise check if origin is in allowed list\n if (config.corsOrigins.includes(origin)) {\n return origin;\n }\n // Default to first allowed origin\n return config.corsOrigins[0];\n },\n allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\n allowHeaders: ['Content-Type', 'Origin', 'Authorization'],\n exposeHeaders: ['Content-Type'],\n maxAge: 600,\n credentials: true,\n }));\n\n /**\n * GET /\n * Returns server version information\n */\n app.get('/', (c) => {\n return c.json({\n version: config.version,\n name: 'Rondevu',\n description: 'Topic-based peer discovery and signaling server'\n });\n });\n\n /**\n * GET /health\n * Health check endpoint with version\n */\n app.get('/health', (c) => {\n return c.json({\n status: 'ok',\n timestamp: Date.now(),\n version: config.version\n });\n });\n\n /**\n * POST /register\n * Register a new peer and receive credentials\n * Accepts optional peerId in request body for custom peer IDs\n */\n app.post('/register', async (c) => {\n try {\n let peerId: string;\n\n // Check if custom peer ID is provided\n const body = await c.req.json().catch(() => ({}));\n const customPeerId = body.peerId;\n\n if (customPeerId !== undefined) {\n // Validate custom peer ID\n if (typeof customPeerId !== 'string' || customPeerId.length === 0) {\n return c.json({ error: 'Peer ID must be a non-empty string' }, 400);\n }\n\n if (customPeerId.length > 128) {\n return c.json({ error: 'Peer ID must be 128 characters or less' }, 400);\n }\n\n // Check if peer ID is already in use by checking for active offers\n const existingOffers = await storage.getOffersByPeerId(customPeerId);\n if (existingOffers.length > 0) {\n return c.json({ error: 'Peer ID is already in use' }, 409);\n }\n\n peerId = customPeerId;\n } else {\n // Generate new peer ID\n peerId = generatePeerId();\n }\n\n // Encrypt peer ID with server secret (async operation)\n const secret = await encryptPeerId(peerId, config.authSecret);\n\n return c.json({\n peerId,\n secret\n }, 200);\n } catch (err) {\n console.error('Error registering peer:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * POST /offers\n * Creates one or more offers with topics\n * Requires authentication\n */\n app.post('/offers', authMiddleware, async (c) => {\n try {\n const body = await c.req.json();\n const { offers } = body;\n\n if (!Array.isArray(offers) || offers.length === 0) {\n return c.json({ error: 'Missing or invalid required parameter: offers (must be non-empty array)' }, 400);\n }\n\n if (offers.length > config.maxOffersPerRequest) {\n return c.json({ error: `Too many offers. Maximum ${config.maxOffersPerRequest} per request` }, 400);\n }\n\n const peerId = getAuthenticatedPeerId(c);\n\n // Validate and prepare offers\n const offerRequests = [];\n for (const offer of offers) {\n // Validate SDP\n if (!offer.sdp || typeof offer.sdp !== 'string') {\n return c.json({ error: 'Each offer must have an sdp field' }, 400);\n }\n\n if (offer.sdp.length > 65536) {\n return c.json({ error: 'SDP must be 64KB or less' }, 400);\n }\n\n // Validate secret if provided\n if (offer.secret !== undefined) {\n if (typeof offer.secret !== 'string') {\n return c.json({ error: 'Secret must be a string' }, 400);\n }\n if (offer.secret.length > 128) {\n return c.json({ error: 'Secret must be 128 characters or less' }, 400);\n }\n }\n\n // Validate topics\n if (!Array.isArray(offer.topics) || offer.topics.length === 0) {\n return c.json({ error: 'Each offer must have a non-empty topics array' }, 400);\n }\n\n if (offer.topics.length > config.maxTopicsPerOffer) {\n return c.json({ error: `Too many topics. Maximum ${config.maxTopicsPerOffer} per offer` }, 400);\n }\n\n for (const topic of offer.topics) {\n if (typeof topic !== 'string' || topic.length === 0 || topic.length > 256) {\n return c.json({ error: 'Each topic must be a string between 1 and 256 characters' }, 400);\n }\n }\n\n // Validate and clamp TTL\n let ttl = offer.ttl || config.offerDefaultTtl;\n if (ttl < config.offerMinTtl) {\n ttl = config.offerMinTtl;\n }\n if (ttl > config.offerMaxTtl) {\n ttl = config.offerMaxTtl;\n }\n\n offerRequests.push({\n id: offer.id,\n peerId,\n sdp: offer.sdp,\n topics: offer.topics,\n expiresAt: Date.now() + ttl,\n secret: offer.secret,\n });\n }\n\n // Create offers\n const createdOffers = await storage.createOffers(offerRequests);\n\n // Return simplified response\n return c.json({\n offers: createdOffers.map(o => ({\n id: o.id,\n peerId: o.peerId,\n topics: o.topics,\n expiresAt: o.expiresAt\n }))\n }, 200);\n } catch (err) {\n console.error('Error creating offers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/by-topic/:topic\n * Find offers by topic with optional bloom filter exclusion\n * Public endpoint (no auth required)\n */\n app.get('/offers/by-topic/:topic', async (c) => {\n try {\n const topic = c.req.param('topic');\n const bloomParam = c.req.query('bloom');\n const limitParam = c.req.query('limit');\n\n const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;\n\n // Parse bloom filter if provided\n let excludePeerIds: string[] = [];\n if (bloomParam) {\n const bloom = parseBloomFilter(bloomParam);\n if (!bloom) {\n return c.json({ error: 'Invalid bloom filter format' }, 400);\n }\n\n // Get all offers for topic first\n const allOffers = await storage.getOffersByTopic(topic);\n\n // Test each peer ID against bloom filter\n const excludeSet = new Set<string>();\n for (const offer of allOffers) {\n if (bloom.test(offer.peerId)) {\n excludeSet.add(offer.peerId);\n }\n }\n\n excludePeerIds = Array.from(excludeSet);\n }\n\n // Get filtered offers\n let offers = await storage.getOffersByTopic(topic, excludePeerIds.length > 0 ? excludePeerIds : undefined);\n\n // Apply limit\n const total = offers.length;\n offers = offers.slice(0, limit);\n\n return c.json({\n topic,\n offers: offers.map(o => ({\n id: o.id,\n peerId: o.peerId,\n sdp: o.sdp,\n topics: o.topics,\n expiresAt: o.expiresAt,\n lastSeen: o.lastSeen,\n hasSecret: !!o.secret // Indicate if secret is required without exposing it\n })),\n total: bloomParam ? total + excludePeerIds.length : total,\n returned: offers.length\n }, 200);\n } catch (err) {\n console.error('Error fetching offers by topic:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /topics\n * List all topics with active peer counts (paginated)\n * Public endpoint (no auth required)\n * Query params:\n * - limit: Max topics to return (default 50, max 200)\n * - offset: Number of topics to skip (default 0)\n * - startsWith: Filter topics starting with this prefix (optional)\n */\n app.get('/topics', async (c) => {\n try {\n const limitParam = c.req.query('limit');\n const offsetParam = c.req.query('offset');\n const startsWithParam = c.req.query('startsWith');\n\n const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;\n const offset = offsetParam ? parseInt(offsetParam, 10) : 0;\n const startsWith = startsWithParam || undefined;\n\n const result = await storage.getTopics(limit, offset, startsWith);\n\n return c.json({\n topics: result.topics,\n total: result.total,\n limit,\n offset,\n ...(startsWith && { startsWith })\n }, 200);\n } catch (err) {\n console.error('Error fetching topics:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /peers/:peerId/offers\n * View all offers from a specific peer\n * Public endpoint\n */\n app.get('/peers/:peerId/offers', async (c) => {\n try {\n const peerId = c.req.param('peerId');\n const offers = await storage.getOffersByPeerId(peerId);\n\n // Collect unique topics\n const topicsSet = new Set<string>();\n offers.forEach(o => o.topics.forEach(t => topicsSet.add(t)));\n\n return c.json({\n peerId,\n offers: offers.map(o => ({\n id: o.id,\n sdp: o.sdp,\n topics: o.topics,\n expiresAt: o.expiresAt,\n lastSeen: o.lastSeen,\n hasSecret: !!o.secret // Indicate if secret is required without exposing it\n })),\n topics: Array.from(topicsSet)\n }, 200);\n } catch (err) {\n console.error('Error fetching peer offers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/mine\n * List all offers owned by authenticated peer\n * Requires authentication\n */\n app.get('/offers/mine', authMiddleware, async (c) => {\n try {\n const peerId = getAuthenticatedPeerId(c);\n const offers = await storage.getOffersByPeerId(peerId);\n\n return c.json({\n peerId,\n offers: offers.map(o => ({\n id: o.id,\n sdp: o.sdp,\n topics: o.topics,\n createdAt: o.createdAt,\n expiresAt: o.expiresAt,\n lastSeen: o.lastSeen,\n secret: o.secret, // Owner can see the secret\n answererPeerId: o.answererPeerId,\n answeredAt: o.answeredAt\n }))\n }, 200);\n } catch (err) {\n console.error('Error fetching own offers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * DELETE /offers/:offerId\n * Delete a specific offer\n * Requires authentication and ownership\n */\n app.delete('/offers/:offerId', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n\n const deleted = await storage.deleteOffer(offerId, peerId);\n\n if (!deleted) {\n return c.json({ error: 'Offer not found or not authorized' }, 404);\n }\n\n return c.json({ deleted: true }, 200);\n } catch (err) {\n console.error('Error deleting offer:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * POST /offers/:offerId/answer\n * Answer a specific offer (locks it to answerer)\n * Requires authentication\n */\n app.post('/offers/:offerId/answer', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n const body = await c.req.json();\n const { sdp, secret } = body;\n\n if (!sdp || typeof sdp !== 'string') {\n return c.json({ error: 'Missing or invalid required parameter: sdp' }, 400);\n }\n\n if (sdp.length > 65536) {\n return c.json({ error: 'SDP must be 64KB or less' }, 400);\n }\n\n // Validate secret if provided\n if (secret !== undefined && typeof secret !== 'string') {\n return c.json({ error: 'Secret must be a string' }, 400);\n }\n\n const result = await storage.answerOffer(offerId, peerId, sdp, secret);\n\n if (!result.success) {\n return c.json({ error: result.error }, 400);\n }\n\n return c.json({\n offerId,\n answererId: peerId,\n answeredAt: Date.now()\n }, 200);\n } catch (err) {\n console.error('Error answering offer:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/answers\n * Poll for answers to all of authenticated peer's offers\n * Requires authentication (offerer)\n */\n app.get('/offers/answers', authMiddleware, async (c) => {\n try {\n const peerId = getAuthenticatedPeerId(c);\n const offers = await storage.getAnsweredOffers(peerId);\n\n return c.json({\n answers: offers.map(o => ({\n offerId: o.id,\n answererId: o.answererPeerId,\n sdp: o.answerSdp,\n answeredAt: o.answeredAt,\n topics: o.topics\n }))\n }, 200);\n } catch (err) {\n console.error('Error fetching answers:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * POST /offers/:offerId/ice-candidates\n * Post ICE candidates for an offer\n * Requires authentication (must be offerer or answerer)\n */\n app.post('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n const body = await c.req.json();\n const { candidates } = body;\n\n if (!Array.isArray(candidates) || candidates.length === 0) {\n return c.json({ error: 'Missing or invalid required parameter: candidates (must be non-empty array)' }, 400);\n }\n\n // Verify offer exists and caller is offerer or answerer\n const offer = await storage.getOfferById(offerId);\n if (!offer) {\n return c.json({ error: 'Offer not found or expired' }, 404);\n }\n\n let role: 'offerer' | 'answerer';\n if (offer.peerId === peerId) {\n role = 'offerer';\n } else if (offer.answererPeerId === peerId) {\n role = 'answerer';\n } else {\n return c.json({ error: 'Not authorized to post ICE candidates for this offer' }, 403);\n }\n\n const added = await storage.addIceCandidates(offerId, peerId, role, candidates);\n\n return c.json({\n offerId,\n candidatesAdded: added\n }, 200);\n } catch (err) {\n console.error('Error adding ICE candidates:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n /**\n * GET /offers/:offerId/ice-candidates\n * Poll for ICE candidates from the other peer\n * Requires authentication (must be offerer or answerer)\n */\n app.get('/offers/:offerId/ice-candidates', authMiddleware, async (c) => {\n try {\n const offerId = c.req.param('offerId');\n const peerId = getAuthenticatedPeerId(c);\n const sinceParam = c.req.query('since');\n\n const since = sinceParam ? parseInt(sinceParam, 10) : undefined;\n\n // Verify offer exists and caller is offerer or answerer\n const offer = await storage.getOfferById(offerId);\n if (!offer) {\n return c.json({ error: 'Offer not found or expired' }, 404);\n }\n\n let targetRole: 'offerer' | 'answerer';\n if (offer.peerId === peerId) {\n // Offerer wants answerer's candidates\n targetRole = 'answerer';\n console.log(`[ICE GET] Offerer ${peerId} requesting answerer ICE candidates for offer ${offerId}, since=${since}, answererPeerId=${offer.answererPeerId}`);\n } else if (offer.answererPeerId === peerId) {\n // Answerer wants offerer's candidates\n targetRole = 'offerer';\n console.log(`[ICE GET] Answerer ${peerId} requesting offerer ICE candidates for offer ${offerId}, since=${since}, offererPeerId=${offer.peerId}`);\n } else {\n return c.json({ error: 'Not authorized to view ICE candidates for this offer' }, 403);\n }\n\n const candidates = await storage.getIceCandidates(offerId, targetRole, since);\n console.log(`[ICE GET] Found ${candidates.length} candidates for offer ${offerId}, targetRole=${targetRole}, since=${since}`);\n\n return c.json({\n offerId,\n candidates: candidates.map(c => ({\n candidate: c.candidate,\n peerId: c.peerId,\n role: c.role,\n createdAt: c.createdAt\n }))\n }, 200);\n } catch (err) {\n console.error('Error fetching ICE candidates:', err);\n return c.json({ error: 'Internal server error' }, 500);\n }\n });\n\n return app;\n}\n", "/**\n * Crypto utilities for stateless peer authentication\n * Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers\n */\n\nconst ALGORITHM = 'AES-GCM';\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst KEY_LENGTH = 32; // 256 bits\n\n/**\n * Generates a random peer ID (16 bytes = 32 hex chars)\n */\nexport function generatePeerId(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Generates a random secret key for encryption (32 bytes = 64 hex chars)\n */\nexport function generateSecretKey(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));\n return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * Convert hex string to Uint8Array\n */\nfunction hexToBytes(hex: string): Uint8Array {\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < hex.length; i += 2) {\n bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Convert Uint8Array to base64 string\n */\nfunction bytesToBase64(bytes: Uint8Array): string {\n const binString = Array.from(bytes, (byte) =>\n String.fromCodePoint(byte)\n ).join('');\n return btoa(binString);\n}\n\n/**\n * Convert base64 string to Uint8Array\n */\nfunction base64ToBytes(base64: string): Uint8Array {\n const binString = atob(base64);\n return Uint8Array.from(binString, (char) => char.codePointAt(0)!);\n}\n\n/**\n * Encrypts a peer ID using the server secret key\n * Returns base64-encoded encrypted data (IV + ciphertext)\n */\nexport async function encryptPeerId(peerId: string, secretKeyHex: string): Promise<string> {\n const keyBytes = hexToBytes(secretKeyHex);\n\n if (keyBytes.length !== KEY_LENGTH) {\n throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);\n }\n\n // Import key\n const key = await crypto.subtle.importKey(\n 'raw',\n keyBytes,\n { name: ALGORITHM, length: 256 },\n false,\n ['encrypt']\n );\n\n // Generate random IV\n const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));\n\n // Encrypt peer ID\n const encoder = new TextEncoder();\n const data = encoder.encode(peerId);\n\n const encrypted = await crypto.subtle.encrypt(\n { name: ALGORITHM, iv },\n key,\n data\n );\n\n // Combine IV + ciphertext and encode as base64\n const combined = new Uint8Array(iv.length + encrypted.byteLength);\n combined.set(iv, 0);\n combined.set(new Uint8Array(encrypted), iv.length);\n\n return bytesToBase64(combined);\n}\n\n/**\n * Decrypts an encrypted peer ID secret\n * Returns the plaintext peer ID or throws if decryption fails\n */\nexport async function decryptPeerId(encryptedSecret: string, secretKeyHex: string): Promise<string> {\n try {\n const keyBytes = hexToBytes(secretKeyHex);\n\n if (keyBytes.length !== KEY_LENGTH) {\n throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);\n }\n\n // Decode base64\n const combined = base64ToBytes(encryptedSecret);\n\n // Extract IV and ciphertext\n const iv = combined.slice(0, IV_LENGTH);\n const ciphertext = combined.slice(IV_LENGTH);\n\n // Import key\n const key = await crypto.subtle.importKey(\n 'raw',\n keyBytes,\n { name: ALGORITHM, length: 256 },\n false,\n ['decrypt']\n );\n\n // Decrypt\n const decrypted = await crypto.subtle.decrypt(\n { name: ALGORITHM, iv },\n key,\n ciphertext\n );\n\n const decoder = new TextDecoder();\n return decoder.decode(decrypted);\n } catch (err) {\n throw new Error('Failed to decrypt peer ID: invalid secret or secret key');\n }\n}\n\n/**\n * Validates that a peer ID and secret match\n * Returns true if valid, false otherwise\n */\nexport async function validateCredentials(peerId: string, encryptedSecret: string, secretKey: string): Promise<boolean> {\n try {\n const decryptedPeerId = await decryptPeerId(encryptedSecret, secretKey);\n return decryptedPeerId === peerId;\n } catch {\n return false;\n }\n}\n", "import { Context, Next } from 'hono';\nimport { validateCredentials } from '../crypto.ts';\n\n/**\n * Authentication middleware for Rondevu\n * Validates Bearer token in format: {peerId}:{encryptedSecret}\n */\nexport function createAuthMiddleware(authSecret: string) {\n return async (c: Context, next: Next) => {\n const authHeader = c.req.header('Authorization');\n\n if (!authHeader) {\n return c.json({ error: 'Missing Authorization header' }, 401);\n }\n\n // Expect format: Bearer {peerId}:{secret}\n const parts = authHeader.split(' ');\n if (parts.length !== 2 || parts[0] !== 'Bearer') {\n return c.json({ error: 'Invalid Authorization header format. Expected: Bearer {peerId}:{secret}' }, 401);\n }\n\n const credentials = parts[1].split(':');\n if (credentials.length !== 2) {\n return c.json({ error: 'Invalid credentials format. Expected: {peerId}:{secret}' }, 401);\n }\n\n const [peerId, encryptedSecret] = credentials;\n\n // Validate credentials (async operation)\n const isValid = await validateCredentials(peerId, encryptedSecret, authSecret);\n if (!isValid) {\n return c.json({ error: 'Invalid credentials' }, 401);\n }\n\n // Attach peer ID to context for use in handlers\n c.set('peerId', peerId);\n\n await next();\n };\n}\n\n/**\n * Helper to get authenticated peer ID from context\n */\nexport function getAuthenticatedPeerId(c: Context): string {\n const peerId = c.get('peerId');\n if (!peerId) {\n throw new Error('No authenticated peer ID in context');\n }\n return peerId;\n}\n", "/**\n * Bloom filter utility for testing if peer IDs might be in a set\n * Used to filter out known peers from discovery results\n */\n\nexport class BloomFilter {\n private bits: Uint8Array;\n private size: number;\n private numHashes: number;\n\n /**\n * Creates a bloom filter from a base64 encoded bit array\n */\n constructor(base64Data: string, numHashes: number = 3) {\n // Decode base64 to Uint8Array (works in both Node.js and Workers)\n const binaryString = atob(base64Data);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n this.bits = bytes;\n this.size = this.bits.length * 8;\n this.numHashes = numHashes;\n }\n\n /**\n * Test if a peer ID might be in the filter\n * Returns true if possibly in set, false if definitely not in set\n */\n test(peerId: string): boolean {\n for (let i = 0; i < this.numHashes; i++) {\n const hash = this.hash(peerId, i);\n const index = hash % this.size;\n const byteIndex = Math.floor(index / 8);\n const bitIndex = index % 8;\n\n if (!(this.bits[byteIndex] & (1 << bitIndex))) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Simple hash function (FNV-1a variant)\n */\n private hash(str: string, seed: number): number {\n let hash = 2166136261 ^ seed;\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i);\n hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n }\n return hash >>> 0;\n }\n}\n\n/**\n * Helper to parse bloom filter from base64 string\n */\nexport function parseBloomFilter(base64: string): BloomFilter | null {\n try {\n return new BloomFilter(base64);\n } catch {\n return null;\n }\n}\n", "import { generateSecretKey } from './crypto.ts';\n\n/**\n * Application configuration\n * Reads from environment variables with sensible defaults\n */\nexport interface Config {\n port: number;\n storageType: 'sqlite' | 'memory';\n storagePath: string;\n corsOrigins: string[];\n version: string;\n authSecret: string;\n offerDefaultTtl: number;\n offerMaxTtl: number;\n offerMinTtl: number;\n cleanupInterval: number;\n maxOffersPerRequest: number;\n maxTopicsPerOffer: number;\n}\n\n/**\n * Loads configuration from environment variables\n */\nexport function loadConfig(): Config {\n // Generate or load auth secret\n let authSecret = process.env.AUTH_SECRET;\n if (!authSecret) {\n authSecret = generateSecretKey();\n console.warn('WARNING: No AUTH_SECRET provided. Generated temporary secret:', authSecret);\n console.warn('All peer credentials will be invalidated on server restart.');\n console.warn('Set AUTH_SECRET environment variable to persist credentials across restarts.');\n }\n\n return {\n port: parseInt(process.env.PORT || '3000', 10),\n storageType: (process.env.STORAGE_TYPE || 'sqlite') as 'sqlite' | 'memory',\n storagePath: process.env.STORAGE_PATH || ':memory:',\n corsOrigins: process.env.CORS_ORIGINS\n ? process.env.CORS_ORIGINS.split(',').map(o => o.trim())\n : ['*'],\n version: process.env.VERSION || 'unknown',\n authSecret,\n offerDefaultTtl: parseInt(process.env.OFFER_DEFAULT_TTL || '60000', 10),\n offerMaxTtl: parseInt(process.env.OFFER_MAX_TTL || '86400000', 10),\n offerMinTtl: parseInt(process.env.OFFER_MIN_TTL || '60000', 10),\n cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || '60000', 10),\n maxOffersPerRequest: parseInt(process.env.MAX_OFFERS_PER_REQUEST || '100', 10),\n maxTopicsPerOffer: parseInt(process.env.MAX_TOPICS_PER_OFFER || '50', 10),\n };\n}\n", "import Database from 'better-sqlite3';\nimport { Storage, Offer, IceCandidate, CreateOfferRequest, TopicInfo } from './types.ts';\nimport { generateOfferHash } from './hash-id.ts';\n\n/**\n * SQLite storage adapter for topic-based offer management\n * Supports both file-based and in-memory databases\n */\nexport class SQLiteStorage implements Storage {\n private db: Database.Database;\n\n /**\n * Creates a new SQLite storage instance\n * @param path Path to SQLite database file, or ':memory:' for in-memory database\n */\n constructor(path: string = ':memory:') {\n this.db = new Database(path);\n this.initializeDatabase();\n }\n\n /**\n * Initializes database schema with new topic-based structure\n */\n private initializeDatabase(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS offers (\n id TEXT PRIMARY KEY,\n peer_id TEXT NOT NULL,\n sdp TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n last_seen INTEGER NOT NULL,\n secret TEXT,\n answerer_peer_id TEXT,\n answer_sdp TEXT,\n answered_at INTEGER\n );\n\n CREATE INDEX IF NOT EXISTS idx_offers_peer ON offers(peer_id);\n CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);\n CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);\n CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);\n\n CREATE TABLE IF NOT EXISTS offer_topics (\n offer_id TEXT NOT NULL,\n topic TEXT NOT NULL,\n PRIMARY KEY (offer_id, topic),\n FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_topics_topic ON offer_topics(topic);\n CREATE INDEX IF NOT EXISTS idx_topics_offer ON offer_topics(offer_id);\n\n CREATE TABLE IF NOT EXISTS ice_candidates (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n offer_id TEXT NOT NULL,\n peer_id TEXT NOT NULL,\n role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),\n candidate TEXT NOT NULL, -- JSON: RTCIceCandidateInit object\n created_at INTEGER NOT NULL,\n FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);\n CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);\n CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);\n `);\n\n // Enable foreign keys\n this.db.pragma('foreign_keys = ON');\n }\n\n async createOffers(offers: CreateOfferRequest[]): Promise<Offer[]> {\n const created: Offer[] = [];\n\n // Generate hash-based IDs for all offers first\n const offersWithIds = await Promise.all(\n offers.map(async (offer) => ({\n ...offer,\n id: offer.id || await generateOfferHash(offer.sdp, offer.topics),\n }))\n );\n\n // Use transaction for atomic creation\n const transaction = this.db.transaction((offersWithIds: (CreateOfferRequest & { id: string })[]) => {\n const offerStmt = this.db.prepare(`\n INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n const topicStmt = this.db.prepare(`\n INSERT INTO offer_topics (offer_id, topic)\n VALUES (?, ?)\n `);\n\n for (const offer of offersWithIds) {\n const now = Date.now();\n\n // Insert offer\n offerStmt.run(\n offer.id,\n offer.peerId,\n offer.sdp,\n now,\n offer.expiresAt,\n now,\n offer.secret || null\n );\n\n // Insert topics\n for (const topic of offer.topics) {\n topicStmt.run(offer.id, topic);\n }\n\n created.push({\n id: offer.id,\n peerId: offer.peerId,\n sdp: offer.sdp,\n topics: offer.topics,\n createdAt: now,\n expiresAt: offer.expiresAt,\n lastSeen: now,\n secret: offer.secret,\n });\n }\n });\n\n transaction(offersWithIds);\n return created;\n }\n\n async getOffersByTopic(topic: string, excludePeerIds?: string[]): Promise<Offer[]> {\n let query = `\n SELECT DISTINCT o.*\n FROM offers o\n INNER JOIN offer_topics ot ON o.id = ot.offer_id\n WHERE ot.topic = ? AND o.expires_at > ?\n `;\n\n const params: any[] = [topic, Date.now()];\n\n if (excludePeerIds && excludePeerIds.length > 0) {\n const placeholders = excludePeerIds.map(() => '?').join(',');\n query += ` AND o.peer_id NOT IN (${placeholders})`;\n params.push(...excludePeerIds);\n }\n\n query += ' ORDER BY o.last_seen DESC';\n\n const stmt = this.db.prepare(query);\n const rows = stmt.all(...params) as any[];\n\n return Promise.all(rows.map(row => this.rowToOffer(row)));\n }\n\n async getOffersByPeerId(peerId: string): Promise<Offer[]> {\n const stmt = this.db.prepare(`\n SELECT * FROM offers\n WHERE peer_id = ? AND expires_at > ?\n ORDER BY last_seen DESC\n `);\n\n const rows = stmt.all(peerId, Date.now()) as any[];\n return Promise.all(rows.map(row => this.rowToOffer(row)));\n }\n\n async getOfferById(offerId: string): Promise<Offer | null> {\n const stmt = this.db.prepare(`\n SELECT * FROM offers\n WHERE id = ? AND expires_at > ?\n `);\n\n const row = stmt.get(offerId, Date.now()) as any;\n\n if (!row) {\n return null;\n }\n\n return this.rowToOffer(row);\n }\n\n async deleteOffer(offerId: string, ownerPeerId: string): Promise<boolean> {\n const stmt = this.db.prepare(`\n DELETE FROM offers\n WHERE id = ? AND peer_id = ?\n `);\n\n const result = stmt.run(offerId, ownerPeerId);\n return result.changes > 0;\n }\n\n async deleteExpiredOffers(now: number): Promise<number> {\n const stmt = this.db.prepare('DELETE FROM offers WHERE expires_at < ?');\n const result = stmt.run(now);\n return result.changes;\n }\n\n async answerOffer(\n offerId: string,\n answererPeerId: string,\n answerSdp: string,\n secret?: string\n ): Promise<{ success: boolean; error?: string }> {\n // Check if offer exists and is not expired\n const offer = await this.getOfferById(offerId);\n\n if (!offer) {\n return {\n success: false,\n error: 'Offer not found or expired'\n };\n }\n\n // Verify secret if offer is protected\n if (offer.secret && offer.secret !== secret) {\n return {\n success: false,\n error: 'Invalid or missing secret'\n };\n }\n\n // Check if offer already has an answerer\n if (offer.answererPeerId) {\n return {\n success: false,\n error: 'Offer already answered'\n };\n }\n\n // Update offer with answer\n const stmt = this.db.prepare(`\n UPDATE offers\n SET answerer_peer_id = ?, answer_sdp = ?, answered_at = ?\n WHERE id = ? AND answerer_peer_id IS NULL\n `);\n\n const result = stmt.run(answererPeerId, answerSdp, Date.now(), offerId);\n\n if (result.changes === 0) {\n return {\n success: false,\n error: 'Offer already answered (race condition)'\n };\n }\n\n return { success: true };\n }\n\n async getAnsweredOffers(offererPeerId: string): Promise<Offer[]> {\n const stmt = this.db.prepare(`\n SELECT * FROM offers\n WHERE peer_id = ? AND answerer_peer_id IS NOT NULL AND expires_at > ?\n ORDER BY answered_at DESC\n `);\n\n const rows = stmt.all(offererPeerId, Date.now()) as any[];\n return Promise.all(rows.map(row => this.rowToOffer(row)));\n }\n\n async addIceCandidates(\n offerId: string,\n peerId: string,\n role: 'offerer' | 'answerer',\n candidates: any[]\n ): Promise<number> {\n const stmt = this.db.prepare(`\n INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)\n VALUES (?, ?, ?, ?, ?)\n `);\n\n const baseTimestamp = Date.now();\n const transaction = this.db.transaction((candidates: any[]) => {\n for (let i = 0; i < candidates.length; i++) {\n stmt.run(\n offerId,\n peerId,\n role,\n JSON.stringify(candidates[i]), // Store full object as JSON\n baseTimestamp + i // Ensure unique timestamps to avoid \"since\" filtering issues\n );\n }\n });\n\n transaction(candidates);\n return candidates.length;\n }\n\n async getIceCandidates(\n offerId: string,\n targetRole: 'offerer' | 'answerer',\n since?: number\n ): Promise<IceCandidate[]> {\n let query = `\n SELECT * FROM ice_candidates\n WHERE offer_id = ? AND role = ?\n `;\n\n const params: any[] = [offerId, targetRole];\n\n if (since !== undefined) {\n query += ' AND created_at > ?';\n params.push(since);\n }\n\n query += ' ORDER BY created_at ASC';\n\n const stmt = this.db.prepare(query);\n const rows = stmt.all(...params) as any[];\n\n return rows.map(row => ({\n id: row.id,\n offerId: row.offer_id,\n peerId: row.peer_id,\n role: row.role,\n candidate: JSON.parse(row.candidate), // Parse JSON back to object\n createdAt: row.created_at,\n }));\n }\n\n async getTopics(limit: number, offset: number, startsWith?: string): Promise<{\n topics: TopicInfo[];\n total: number;\n }> {\n const now = Date.now();\n\n // Build WHERE clause for startsWith filter\n const whereClause = startsWith\n ? 'o.expires_at > ? AND ot.topic LIKE ?'\n : 'o.expires_at > ?';\n\n const startsWithPattern = startsWith ? `${startsWith}%` : null;\n\n // Get total count of topics with active offers\n const countQuery = `\n SELECT COUNT(DISTINCT ot.topic) as count\n FROM offer_topics ot\n INNER JOIN offers o ON ot.offer_id = o.id\n WHERE ${whereClause}\n `;\n\n const countStmt = this.db.prepare(countQuery);\n const countParams = startsWith ? [now, startsWithPattern] : [now];\n const countRow = countStmt.get(...countParams) as any;\n const total = countRow.count;\n\n // Get topics with peer counts (paginated)\n const topicsQuery = `\n SELECT\n ot.topic,\n COUNT(DISTINCT o.peer_id) as active_peers\n FROM offer_topics ot\n INNER JOIN offers o ON ot.offer_id = o.id\n WHERE ${whereClause}\n GROUP BY ot.topic\n ORDER BY active_peers DESC, ot.topic ASC\n LIMIT ? OFFSET ?\n `;\n\n const topicsStmt = this.db.prepare(topicsQuery);\n const topicsParams = startsWith\n ? [now, startsWithPattern, limit, offset]\n : [now, limit, offset];\n const rows = topicsStmt.all(...topicsParams) as any[];\n\n const topics = rows.map(row => ({\n topic: row.topic,\n activePeers: row.active_peers,\n }));\n\n return { topics, total };\n }\n\n async close(): Promise<void> {\n this.db.close();\n }\n\n /**\n * Helper method to convert database row to Offer object with topics\n */\n private async rowToOffer(row: any): Promise<Offer> {\n // Get topics for this offer\n const topicStmt = this.db.prepare(`\n SELECT topic FROM offer_topics WHERE offer_id = ?\n `);\n\n const topicRows = topicStmt.all(row.id) as any[];\n const topics = topicRows.map(t => t.topic);\n\n return {\n id: row.id,\n peerId: row.peer_id,\n sdp: row.sdp,\n topics,\n createdAt: row.created_at,\n expiresAt: row.expires_at,\n lastSeen: row.last_seen,\n secret: row.secret || undefined,\n answererPeerId: row.answerer_peer_id || undefined,\n answerSdp: row.answer_sdp || undefined,\n answeredAt: row.answered_at || undefined,\n };\n }\n}\n", "/**\n * Generates a content-based offer ID using SHA-256 hash\n * Creates deterministic IDs based on offer content (sdp, topics)\n * PeerID is not included as it's inferred from authentication\n * Uses Web Crypto API for compatibility with both Node.js and Cloudflare Workers\n *\n * @param sdp - The WebRTC SDP offer\n * @param topics - Array of topic strings\n * @returns SHA-256 hash of the sanitized offer content\n */\nexport async function generateOfferHash(\n sdp: string,\n topics: string[]\n): Promise<string> {\n // Sanitize and normalize the offer content\n // Only include core offer content (not peerId - that's inferred from auth)\n const sanitizedOffer = {\n sdp,\n topics: [...topics].sort(), // Sort topics for consistency\n };\n\n // Create non-prettified JSON string\n const jsonString = JSON.stringify(sanitizedOffer);\n\n // Convert string to Uint8Array for hashing\n const encoder = new TextEncoder();\n const data = encoder.encode(jsonString);\n\n // Generate SHA-256 hash\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n\n // Convert hash to hex string\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n\n return hashHex;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yBAAsB;;;ACAtB,kBAAqB;AACrB,kBAAqB;;;ACIrB,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,aAAa;AAKZ,SAAS,iBAAyB;AACvC,QAAM,QAAQ,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AACvD,SAAO,MAAM,KAAK,KAAK,EAAE,IAAI,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC5E;AAKO,SAAS,oBAA4B;AAC1C,QAAM,QAAQ,OAAO,gBAAgB,IAAI,WAAW,UAAU,CAAC;AAC/D,SAAO,MAAM,KAAK,KAAK,EAAE,IAAI,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC5E;AAKA,SAAS,WAAW,KAAyB;AAC3C,QAAM,QAAQ,IAAI,WAAW,IAAI,SAAS,CAAC;AAC3C,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,GAAG;AACtC,UAAM,IAAI,CAAC,IAAI,SAAS,IAAI,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE;AAAA,EACrD;AACA,SAAO;AACT;AAKA,SAAS,cAAc,OAA2B;AAChD,QAAM,YAAY,MAAM;AAAA,IAAK;AAAA,IAAO,CAAC,SACnC,OAAO,cAAc,IAAI;AAAA,EAC3B,EAAE,KAAK,EAAE;AACT,SAAO,KAAK,SAAS;AACvB;AAKA,SAAS,cAAc,QAA4B;AACjD,QAAM,YAAY,KAAK,MAAM;AAC7B,SAAO,WAAW,KAAK,WAAW,CAAC,SAAS,KAAK,YAAY,CAAC,CAAE;AAClE;AAMA,eAAsB,cAAc,QAAgB,cAAuC;AACzF,QAAM,WAAW,WAAW,YAAY;AAExC,MAAI,SAAS,WAAW,YAAY;AAClC,UAAM,IAAI,MAAM,sBAAsB,aAAa,CAAC,oBAAoB,UAAU,SAAS;AAAA,EAC7F;AAGA,QAAM,MAAM,MAAM,OAAO,OAAO;AAAA,IAC9B;AAAA,IACA;AAAA,IACA,EAAE,MAAM,WAAW,QAAQ,IAAI;AAAA,IAC/B;AAAA,IACA,CAAC,SAAS;AAAA,EACZ;AAGA,QAAM,KAAK,OAAO,gBAAgB,IAAI,WAAW,SAAS,CAAC;AAG3D,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,MAAM;AAElC,QAAM,YAAY,MAAM,OAAO,OAAO;AAAA,IACpC,EAAE,MAAM,WAAW,GAAG;AAAA,IACtB;AAAA,IACA;AAAA,EACF;AAGA,QAAM,WAAW,IAAI,WAAW,GAAG,SAAS,UAAU,UAAU;AAChE,WAAS,IAAI,IAAI,CAAC;AAClB,WAAS,IAAI,IAAI,WAAW,SAAS,GAAG,GAAG,MAAM;AAEjD,SAAO,cAAc,QAAQ;AAC/B;AAMA,eAAsB,cAAc,iBAAyB,cAAuC;AAClG,MAAI;AACF,UAAM,WAAW,WAAW,YAAY;AAExC,QAAI,SAAS,WAAW,YAAY;AAClC,YAAM,IAAI,MAAM,sBAAsB,aAAa,CAAC,oBAAoB,UAAU,SAAS;AAAA,IAC7F;AAGA,UAAM,WAAW,cAAc,eAAe;AAG9C,UAAM,KAAK,SAAS,MAAM,GAAG,SAAS;AACtC,UAAM,aAAa,SAAS,MAAM,SAAS;AAG3C,UAAM,MAAM,MAAM,OAAO,OAAO;AAAA,MAC9B;AAAA,MACA;AAAA,MACA,EAAE,MAAM,WAAW,QAAQ,IAAI;AAAA,MAC/B;AAAA,MACA,CAAC,SAAS;AAAA,IACZ;AAGA,UAAM,YAAY,MAAM,OAAO,OAAO;AAAA,MACpC,EAAE,MAAM,WAAW,GAAG;AAAA,MACtB;AAAA,MACA;AAAA,IACF;AAEA,UAAM,UAAU,IAAI,YAAY;AAChC,WAAO,QAAQ,OAAO,SAAS;AAAA,EACjC,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACF;AAMA,eAAsB,oBAAoB,QAAgB,iBAAyB,WAAqC;AACtH,MAAI;AACF,UAAM,kBAAkB,MAAM,cAAc,iBAAiB,SAAS;AACtE,WAAO,oBAAoB;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AC7IO,SAAS,qBAAqB,YAAoB;AACvD,SAAO,OAAO,GAAY,SAAe;AACvC,UAAM,aAAa,EAAE,IAAI,OAAO,eAAe;AAE/C,QAAI,CAAC,YAAY;AACf,aAAO,EAAE,KAAK,EAAE,OAAO,+BAA+B,GAAG,GAAG;AAAA,IAC9D;AAGA,UAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,QAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,UAAU;AAC/C,aAAO,EAAE,KAAK,EAAE,OAAO,0EAA0E,GAAG,GAAG;AAAA,IACzG;AAEA,UAAM,cAAc,MAAM,CAAC,EAAE,MAAM,GAAG;AACtC,QAAI,YAAY,WAAW,GAAG;AAC5B,aAAO,EAAE,KAAK,EAAE,OAAO,0DAA0D,GAAG,GAAG;AAAA,IACzF;AAEA,UAAM,CAAC,QAAQ,eAAe,IAAI;AAGlC,UAAM,UAAU,MAAM,oBAAoB,QAAQ,iBAAiB,UAAU;AAC7E,QAAI,CAAC,SAAS;AACZ,aAAO,EAAE,KAAK,EAAE,OAAO,sBAAsB,GAAG,GAAG;AAAA,IACrD;AAGA,MAAE,IAAI,UAAU,MAAM;AAEtB,UAAM,KAAK;AAAA,EACb;AACF;AAKO,SAAS,uBAAuB,GAAoB;AACzD,QAAM,SAAS,EAAE,IAAI,QAAQ;AAC7B,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACvD;AACA,SAAO;AACT;;;AC7CO,IAAM,cAAN,MAAkB;AAAA;AAAA;AAAA;AAAA,EAQvB,YAAY,YAAoB,YAAoB,GAAG;AAErD,UAAM,eAAe,KAAK,UAAU;AACpC,UAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;AAChD,aAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,YAAM,CAAC,IAAI,aAAa,WAAW,CAAC;AAAA,IACtC;AACA,SAAK,OAAO;AACZ,SAAK,OAAO,KAAK,KAAK,SAAS;AAC/B,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK,QAAyB;AAC5B,aAAS,IAAI,GAAG,IAAI,KAAK,WAAW,KAAK;AACvC,YAAM,OAAO,KAAK,KAAK,QAAQ,CAAC;AAChC,YAAM,QAAQ,OAAO,KAAK;AAC1B,YAAM,YAAY,KAAK,MAAM,QAAQ,CAAC;AACtC,YAAM,WAAW,QAAQ;AAEzB,UAAI,EAAE,KAAK,KAAK,SAAS,IAAK,KAAK,WAAY;AAC7C,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,KAAK,KAAa,MAAsB;AAC9C,QAAI,OAAO,aAAa;AACxB,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAQ,IAAI,WAAW,CAAC;AACxB,eAAS,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ;AAAA,IAC3E;AACA,WAAO,SAAS;AAAA,EAClB;AACF;AAKO,SAAS,iBAAiB,QAAoC;AACnE,MAAI;AACF,WAAO,IAAI,YAAY,MAAM;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AHrDO,SAAS,UAAU,SAAkB,QAAgB;AAC1D,QAAM,MAAM,IAAI,iBAAK;AAGrB,QAAM,iBAAiB,qBAAqB,OAAO,UAAU;AAG7D,MAAI,IAAI,UAAM,kBAAK;AAAA,IACjB,QAAQ,CAAC,WAAW;AAElB,UAAI,OAAO,YAAY,WAAW,KAAK,OAAO,YAAY,CAAC,MAAM,KAAK;AACpE,eAAO;AAAA,MACT;AAEA,UAAI,OAAO,YAAY,SAAS,MAAM,GAAG;AACvC,eAAO;AAAA,MACT;AAEA,aAAO,OAAO,YAAY,CAAC;AAAA,IAC7B;AAAA,IACA,cAAc,CAAC,OAAO,QAAQ,OAAO,UAAU,SAAS;AAAA,IACxD,cAAc,CAAC,gBAAgB,UAAU,eAAe;AAAA,IACxD,eAAe,CAAC,cAAc;AAAA,IAC9B,QAAQ;AAAA,IACR,aAAa;AAAA,EACf,CAAC,CAAC;AAMF,MAAI,IAAI,KAAK,CAAC,MAAM;AAClB,WAAO,EAAE,KAAK;AAAA,MACZ,SAAS,OAAO;AAAA,MAChB,MAAM;AAAA,MACN,aAAa;AAAA,IACf,CAAC;AAAA,EACH,CAAC;AAMD,MAAI,IAAI,WAAW,CAAC,MAAM;AACxB,WAAO,EAAE,KAAK;AAAA,MACZ,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI;AAAA,MACpB,SAAS,OAAO;AAAA,IAClB,CAAC;AAAA,EACH,CAAC;AAOD,MAAI,KAAK,aAAa,OAAO,MAAM;AACjC,QAAI;AACF,UAAI;AAGJ,YAAM,OAAO,MAAM,EAAE,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAChD,YAAM,eAAe,KAAK;AAE1B,UAAI,iBAAiB,QAAW;AAE9B,YAAI,OAAO,iBAAiB,YAAY,aAAa,WAAW,GAAG;AACjE,iBAAO,EAAE,KAAK,EAAE,OAAO,qCAAqC,GAAG,GAAG;AAAA,QACpE;AAEA,YAAI,aAAa,SAAS,KAAK;AAC7B,iBAAO,EAAE,KAAK,EAAE,OAAO,yCAAyC,GAAG,GAAG;AAAA,QACxE;AAGA,cAAM,iBAAiB,MAAM,QAAQ,kBAAkB,YAAY;AACnE,YAAI,eAAe,SAAS,GAAG;AAC7B,iBAAO,EAAE,KAAK,EAAE,OAAO,4BAA4B,GAAG,GAAG;AAAA,QAC3D;AAEA,iBAAS;AAAA,MACX,OAAO;AAEL,iBAAS,eAAe;AAAA,MAC1B;AAGA,YAAM,SAAS,MAAM,cAAc,QAAQ,OAAO,UAAU;AAE5D,aAAO,EAAE,KAAK;AAAA,QACZ;AAAA,QACA;AAAA,MACF,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,2BAA2B,GAAG;AAC5C,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,KAAK,WAAW,gBAAgB,OAAO,MAAM;AAC/C,QAAI;AACF,YAAM,OAAO,MAAM,EAAE,IAAI,KAAK;AAC9B,YAAM,EAAE,OAAO,IAAI;AAEnB,UAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,GAAG;AACjD,eAAO,EAAE,KAAK,EAAE,OAAO,0EAA0E,GAAG,GAAG;AAAA,MACzG;AAEA,UAAI,OAAO,SAAS,OAAO,qBAAqB;AAC9C,eAAO,EAAE,KAAK,EAAE,OAAO,4BAA4B,OAAO,mBAAmB,eAAe,GAAG,GAAG;AAAA,MACpG;AAEA,YAAM,SAAS,uBAAuB,CAAC;AAGvC,YAAM,gBAAgB,CAAC;AACvB,iBAAW,SAAS,QAAQ;AAE1B,YAAI,CAAC,MAAM,OAAO,OAAO,MAAM,QAAQ,UAAU;AAC/C,iBAAO,EAAE,KAAK,EAAE,OAAO,oCAAoC,GAAG,GAAG;AAAA,QACnE;AAEA,YAAI,MAAM,IAAI,SAAS,OAAO;AAC5B,iBAAO,EAAE,KAAK,EAAE,OAAO,2BAA2B,GAAG,GAAG;AAAA,QAC1D;AAGA,YAAI,MAAM,WAAW,QAAW;AAC9B,cAAI,OAAO,MAAM,WAAW,UAAU;AACpC,mBAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,GAAG,GAAG;AAAA,UACzD;AACA,cAAI,MAAM,OAAO,SAAS,KAAK;AAC7B,mBAAO,EAAE,KAAK,EAAE,OAAO,wCAAwC,GAAG,GAAG;AAAA,UACvE;AAAA,QACF;AAGA,YAAI,CAAC,MAAM,QAAQ,MAAM,MAAM,KAAK,MAAM,OAAO,WAAW,GAAG;AAC7D,iBAAO,EAAE,KAAK,EAAE,OAAO,gDAAgD,GAAG,GAAG;AAAA,QAC/E;AAEA,YAAI,MAAM,OAAO,SAAS,OAAO,mBAAmB;AAClD,iBAAO,EAAE,KAAK,EAAE,OAAO,4BAA4B,OAAO,iBAAiB,aAAa,GAAG,GAAG;AAAA,QAChG;AAEA,mBAAW,SAAS,MAAM,QAAQ;AAChC,cAAI,OAAO,UAAU,YAAY,MAAM,WAAW,KAAK,MAAM,SAAS,KAAK;AACzE,mBAAO,EAAE,KAAK,EAAE,OAAO,2DAA2D,GAAG,GAAG;AAAA,UAC1F;AAAA,QACF;AAGA,YAAI,MAAM,MAAM,OAAO,OAAO;AAC9B,YAAI,MAAM,OAAO,aAAa;AAC5B,gBAAM,OAAO;AAAA,QACf;AACA,YAAI,MAAM,OAAO,aAAa;AAC5B,gBAAM,OAAO;AAAA,QACf;AAEA,sBAAc,KAAK;AAAA,UACjB,IAAI,MAAM;AAAA,UACV;AAAA,UACA,KAAK,MAAM;AAAA,UACX,QAAQ,MAAM;AAAA,UACd,WAAW,KAAK,IAAI,IAAI;AAAA,UACxB,QAAQ,MAAM;AAAA,QAChB,CAAC;AAAA,MACH;AAGA,YAAM,gBAAgB,MAAM,QAAQ,aAAa,aAAa;AAG9D,aAAO,EAAE,KAAK;AAAA,QACZ,QAAQ,cAAc,IAAI,QAAM;AAAA,UAC9B,IAAI,EAAE;AAAA,UACN,QAAQ,EAAE;AAAA,UACV,QAAQ,EAAE;AAAA,UACV,WAAW,EAAE;AAAA,QACf,EAAE;AAAA,MACJ,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,0BAA0B,GAAG;AAC3C,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,IAAI,2BAA2B,OAAO,MAAM;AAC9C,QAAI;AACF,YAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;AACjC,YAAM,aAAa,EAAE,IAAI,MAAM,OAAO;AACtC,YAAM,aAAa,EAAE,IAAI,MAAM,OAAO;AAEtC,YAAM,QAAQ,aAAa,KAAK,IAAI,SAAS,YAAY,EAAE,GAAG,GAAG,IAAI;AAGrE,UAAI,iBAA2B,CAAC;AAChC,UAAI,YAAY;AACd,cAAM,QAAQ,iBAAiB,UAAU;AACzC,YAAI,CAAC,OAAO;AACV,iBAAO,EAAE,KAAK,EAAE,OAAO,8BAA8B,GAAG,GAAG;AAAA,QAC7D;AAGA,cAAM,YAAY,MAAM,QAAQ,iBAAiB,KAAK;AAGtD,cAAM,aAAa,oBAAI,IAAY;AACnC,mBAAW,SAAS,WAAW;AAC7B,cAAI,MAAM,KAAK,MAAM,MAAM,GAAG;AAC5B,uBAAW,IAAI,MAAM,MAAM;AAAA,UAC7B;AAAA,QACF;AAEA,yBAAiB,MAAM,KAAK,UAAU;AAAA,MACxC;AAGA,UAAI,SAAS,MAAM,QAAQ,iBAAiB,OAAO,eAAe,SAAS,IAAI,iBAAiB,MAAS;AAGzG,YAAM,QAAQ,OAAO;AACrB,eAAS,OAAO,MAAM,GAAG,KAAK;AAE9B,aAAO,EAAE,KAAK;AAAA,QACZ;AAAA,QACA,QAAQ,OAAO,IAAI,QAAM;AAAA,UACvB,IAAI,EAAE;AAAA,UACN,QAAQ,EAAE;AAAA,UACV,KAAK,EAAE;AAAA,UACP,QAAQ,EAAE;AAAA,UACV,WAAW,EAAE;AAAA,UACb,UAAU,EAAE;AAAA,UACZ,WAAW,CAAC,CAAC,EAAE;AAAA;AAAA,QACjB,EAAE;AAAA,QACF,OAAO,aAAa,QAAQ,eAAe,SAAS;AAAA,QACpD,UAAU,OAAO;AAAA,MACnB,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,mCAAmC,GAAG;AACpD,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAWD,MAAI,IAAI,WAAW,OAAO,MAAM;AAC9B,QAAI;AACF,YAAM,aAAa,EAAE,IAAI,MAAM,OAAO;AACtC,YAAM,cAAc,EAAE,IAAI,MAAM,QAAQ;AACxC,YAAM,kBAAkB,EAAE,IAAI,MAAM,YAAY;AAEhD,YAAM,QAAQ,aAAa,KAAK,IAAI,SAAS,YAAY,EAAE,GAAG,GAAG,IAAI;AACrE,YAAM,SAAS,cAAc,SAAS,aAAa,EAAE,IAAI;AACzD,YAAM,aAAa,mBAAmB;AAEtC,YAAM,SAAS,MAAM,QAAQ,UAAU,OAAO,QAAQ,UAAU;AAEhE,aAAO,EAAE,KAAK;AAAA,QACZ,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,QACd;AAAA,QACA;AAAA,QACA,GAAI,cAAc,EAAE,WAAW;AAAA,MACjC,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,0BAA0B,GAAG;AAC3C,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,IAAI,yBAAyB,OAAO,MAAM;AAC5C,QAAI;AACF,YAAM,SAAS,EAAE,IAAI,MAAM,QAAQ;AACnC,YAAM,SAAS,MAAM,QAAQ,kBAAkB,MAAM;AAGrD,YAAM,YAAY,oBAAI,IAAY;AAClC,aAAO,QAAQ,OAAK,EAAE,OAAO,QAAQ,OAAK,UAAU,IAAI,CAAC,CAAC,CAAC;AAE3D,aAAO,EAAE,KAAK;AAAA,QACZ;AAAA,QACA,QAAQ,OAAO,IAAI,QAAM;AAAA,UACvB,IAAI,EAAE;AAAA,UACN,KAAK,EAAE;AAAA,UACP,QAAQ,EAAE;AAAA,UACV,WAAW,EAAE;AAAA,UACb,UAAU,EAAE;AAAA,UACZ,WAAW,CAAC,CAAC,EAAE;AAAA;AAAA,QACjB,EAAE;AAAA,QACF,QAAQ,MAAM,KAAK,SAAS;AAAA,MAC9B,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,+BAA+B,GAAG;AAChD,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,IAAI,gBAAgB,gBAAgB,OAAO,MAAM;AACnD,QAAI;AACF,YAAM,SAAS,uBAAuB,CAAC;AACvC,YAAM,SAAS,MAAM,QAAQ,kBAAkB,MAAM;AAErD,aAAO,EAAE,KAAK;AAAA,QACZ;AAAA,QACA,QAAQ,OAAO,IAAI,QAAM;AAAA,UACvB,IAAI,EAAE;AAAA,UACN,KAAK,EAAE;AAAA,UACP,QAAQ,EAAE;AAAA,UACV,WAAW,EAAE;AAAA,UACb,WAAW,EAAE;AAAA,UACb,UAAU,EAAE;AAAA,UACZ,QAAQ,EAAE;AAAA;AAAA,UACV,gBAAgB,EAAE;AAAA,UAClB,YAAY,EAAE;AAAA,QAChB,EAAE;AAAA,MACJ,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,8BAA8B,GAAG;AAC/C,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,OAAO,oBAAoB,gBAAgB,OAAO,MAAM;AAC1D,QAAI;AACF,YAAM,UAAU,EAAE,IAAI,MAAM,SAAS;AACrC,YAAM,SAAS,uBAAuB,CAAC;AAEvC,YAAM,UAAU,MAAM,QAAQ,YAAY,SAAS,MAAM;AAEzD,UAAI,CAAC,SAAS;AACZ,eAAO,EAAE,KAAK,EAAE,OAAO,oCAAoC,GAAG,GAAG;AAAA,MACnE;AAEA,aAAO,EAAE,KAAK,EAAE,SAAS,KAAK,GAAG,GAAG;AAAA,IACtC,SAAS,KAAK;AACZ,cAAQ,MAAM,yBAAyB,GAAG;AAC1C,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,KAAK,2BAA2B,gBAAgB,OAAO,MAAM;AAC/D,QAAI;AACF,YAAM,UAAU,EAAE,IAAI,MAAM,SAAS;AACrC,YAAM,SAAS,uBAAuB,CAAC;AACvC,YAAM,OAAO,MAAM,EAAE,IAAI,KAAK;AAC9B,YAAM,EAAE,KAAK,OAAO,IAAI;AAExB,UAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,eAAO,EAAE,KAAK,EAAE,OAAO,6CAA6C,GAAG,GAAG;AAAA,MAC5E;AAEA,UAAI,IAAI,SAAS,OAAO;AACtB,eAAO,EAAE,KAAK,EAAE,OAAO,2BAA2B,GAAG,GAAG;AAAA,MAC1D;AAGA,UAAI,WAAW,UAAa,OAAO,WAAW,UAAU;AACtD,eAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,GAAG,GAAG;AAAA,MACzD;AAEA,YAAM,SAAS,MAAM,QAAQ,YAAY,SAAS,QAAQ,KAAK,MAAM;AAErE,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO,EAAE,KAAK,EAAE,OAAO,OAAO,MAAM,GAAG,GAAG;AAAA,MAC5C;AAEA,aAAO,EAAE,KAAK;AAAA,QACZ;AAAA,QACA,YAAY;AAAA,QACZ,YAAY,KAAK,IAAI;AAAA,MACvB,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,0BAA0B,GAAG;AAC3C,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,IAAI,mBAAmB,gBAAgB,OAAO,MAAM;AACtD,QAAI;AACF,YAAM,SAAS,uBAAuB,CAAC;AACvC,YAAM,SAAS,MAAM,QAAQ,kBAAkB,MAAM;AAErD,aAAO,EAAE,KAAK;AAAA,QACZ,SAAS,OAAO,IAAI,QAAM;AAAA,UACxB,SAAS,EAAE;AAAA,UACX,YAAY,EAAE;AAAA,UACd,KAAK,EAAE;AAAA,UACP,YAAY,EAAE;AAAA,UACd,QAAQ,EAAE;AAAA,QACZ,EAAE;AAAA,MACJ,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,2BAA2B,GAAG;AAC5C,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,KAAK,mCAAmC,gBAAgB,OAAO,MAAM;AACvE,QAAI;AACF,YAAM,UAAU,EAAE,IAAI,MAAM,SAAS;AACrC,YAAM,SAAS,uBAAuB,CAAC;AACvC,YAAM,OAAO,MAAM,EAAE,IAAI,KAAK;AAC9B,YAAM,EAAE,WAAW,IAAI;AAEvB,UAAI,CAAC,MAAM,QAAQ,UAAU,KAAK,WAAW,WAAW,GAAG;AACzD,eAAO,EAAE,KAAK,EAAE,OAAO,8EAA8E,GAAG,GAAG;AAAA,MAC7G;AAGA,YAAM,QAAQ,MAAM,QAAQ,aAAa,OAAO;AAChD,UAAI,CAAC,OAAO;AACV,eAAO,EAAE,KAAK,EAAE,OAAO,6BAA6B,GAAG,GAAG;AAAA,MAC5D;AAEA,UAAI;AACJ,UAAI,MAAM,WAAW,QAAQ;AAC3B,eAAO;AAAA,MACT,WAAW,MAAM,mBAAmB,QAAQ;AAC1C,eAAO;AAAA,MACT,OAAO;AACL,eAAO,EAAE,KAAK,EAAE,OAAO,uDAAuD,GAAG,GAAG;AAAA,MACtF;AAEA,YAAM,QAAQ,MAAM,QAAQ,iBAAiB,SAAS,QAAQ,MAAM,UAAU;AAE9E,aAAO,EAAE,KAAK;AAAA,QACZ;AAAA,QACA,iBAAiB;AAAA,MACnB,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,gCAAgC,GAAG;AACjD,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAOD,MAAI,IAAI,mCAAmC,gBAAgB,OAAO,MAAM;AACtE,QAAI;AACF,YAAM,UAAU,EAAE,IAAI,MAAM,SAAS;AACrC,YAAM,SAAS,uBAAuB,CAAC;AACvC,YAAM,aAAa,EAAE,IAAI,MAAM,OAAO;AAEtC,YAAM,QAAQ,aAAa,SAAS,YAAY,EAAE,IAAI;AAGtD,YAAM,QAAQ,MAAM,QAAQ,aAAa,OAAO;AAChD,UAAI,CAAC,OAAO;AACV,eAAO,EAAE,KAAK,EAAE,OAAO,6BAA6B,GAAG,GAAG;AAAA,MAC5D;AAEA,UAAI;AACJ,UAAI,MAAM,WAAW,QAAQ;AAE3B,qBAAa;AACb,gBAAQ,IAAI,qBAAqB,MAAM,iDAAiD,OAAO,WAAW,KAAK,oBAAoB,MAAM,cAAc,EAAE;AAAA,MAC3J,WAAW,MAAM,mBAAmB,QAAQ;AAE1C,qBAAa;AACb,gBAAQ,IAAI,sBAAsB,MAAM,gDAAgD,OAAO,WAAW,KAAK,mBAAmB,MAAM,MAAM,EAAE;AAAA,MAClJ,OAAO;AACL,eAAO,EAAE,KAAK,EAAE,OAAO,uDAAuD,GAAG,GAAG;AAAA,MACtF;AAEA,YAAM,aAAa,MAAM,QAAQ,iBAAiB,SAAS,YAAY,KAAK;AAC5E,cAAQ,IAAI,mBAAmB,WAAW,MAAM,yBAAyB,OAAO,gBAAgB,UAAU,WAAW,KAAK,EAAE;AAE5H,aAAO,EAAE,KAAK;AAAA,QACZ;AAAA,QACA,YAAY,WAAW,IAAI,CAAAA,QAAM;AAAA,UAC/B,WAAWA,GAAE;AAAA,UACb,QAAQA,GAAE;AAAA,UACV,MAAMA,GAAE;AAAA,UACR,WAAWA,GAAE;AAAA,QACf,EAAE;AAAA,MACJ,GAAG,GAAG;AAAA,IACR,SAAS,KAAK;AACZ,cAAQ,MAAM,kCAAkC,GAAG;AACnD,aAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAED,SAAO;AACT;;;AI3gBO,SAAS,aAAqB;AAEnC,MAAI,aAAa,QAAQ,IAAI;AAC7B,MAAI,CAAC,YAAY;AACf,iBAAa,kBAAkB;AAC/B,YAAQ,KAAK,iEAAiE,UAAU;AACxF,YAAQ,KAAK,6DAA6D;AAC1E,YAAQ,KAAK,8EAA8E;AAAA,EAC7F;AAEA,SAAO;AAAA,IACL,MAAM,SAAS,QAAQ,IAAI,QAAQ,QAAQ,EAAE;AAAA,IAC7C,aAAc,QAAQ,IAAI,gBAAgB;AAAA,IAC1C,aAAa,QAAQ,IAAI,gBAAgB;AAAA,IACzC,aAAa,QAAQ,IAAI,eACrB,QAAQ,IAAI,aAAa,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC,IACrD,CAAC,GAAG;AAAA,IACR,SAAS,QAAQ,IAAI,WAAW;AAAA,IAChC;AAAA,IACA,iBAAiB,SAAS,QAAQ,IAAI,qBAAqB,SAAS,EAAE;AAAA,IACtE,aAAa,SAAS,QAAQ,IAAI,iBAAiB,YAAY,EAAE;AAAA,IACjE,aAAa,SAAS,QAAQ,IAAI,iBAAiB,SAAS,EAAE;AAAA,IAC9D,iBAAiB,SAAS,QAAQ,IAAI,oBAAoB,SAAS,EAAE;AAAA,IACrE,qBAAqB,SAAS,QAAQ,IAAI,0BAA0B,OAAO,EAAE;AAAA,IAC7E,mBAAmB,SAAS,QAAQ,IAAI,wBAAwB,MAAM,EAAE;AAAA,EAC1E;AACF;;;AClDA,4BAAqB;;;ACUrB,eAAsB,kBACpB,KACA,QACiB;AAGjB,QAAM,iBAAiB;AAAA,IACrB;AAAA,IACA,QAAQ,CAAC,GAAG,MAAM,EAAE,KAAK;AAAA;AAAA,EAC3B;AAGA,QAAM,aAAa,KAAK,UAAU,cAAc;AAGhD,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,UAAU;AAGtC,QAAM,aAAa,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AAG7D,QAAM,YAAY,MAAM,KAAK,IAAI,WAAW,UAAU,CAAC;AACvD,QAAM,UAAU,UAAU,IAAI,OAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAE3E,SAAO;AACT;;;AD5BO,IAAM,gBAAN,MAAuC;AAAA;AAAA;AAAA;AAAA;AAAA,EAO5C,YAAY,OAAe,YAAY;AACrC,SAAK,KAAK,IAAI,sBAAAC,QAAS,IAAI;AAC3B,SAAK,mBAAmB;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAA2B;AACjC,SAAK,GAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KA0CZ;AAGD,SAAK,GAAG,OAAO,mBAAmB;AAAA,EACpC;AAAA,EAEA,MAAM,aAAa,QAAgD;AACjE,UAAM,UAAmB,CAAC;AAG1B,UAAM,gBAAgB,MAAM,QAAQ;AAAA,MAClC,OAAO,IAAI,OAAO,WAAW;AAAA,QAC3B,GAAG;AAAA,QACH,IAAI,MAAM,MAAM,MAAM,kBAAkB,MAAM,KAAK,MAAM,MAAM;AAAA,MACjE,EAAE;AAAA,IACJ;AAGA,UAAM,cAAc,KAAK,GAAG,YAAY,CAACC,mBAA2D;AAClG,YAAM,YAAY,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,OAGjC;AAED,YAAM,YAAY,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,OAGjC;AAED,iBAAW,SAASA,gBAAe;AACjC,cAAM,MAAM,KAAK,IAAI;AAGrB,kBAAU;AAAA,UACR,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN;AAAA,UACA,MAAM;AAAA,UACN;AAAA,UACA,MAAM,UAAU;AAAA,QAClB;AAGA,mBAAW,SAAS,MAAM,QAAQ;AAChC,oBAAU,IAAI,MAAM,IAAI,KAAK;AAAA,QAC/B;AAEA,gBAAQ,KAAK;AAAA,UACX,IAAI,MAAM;AAAA,UACV,QAAQ,MAAM;AAAA,UACd,KAAK,MAAM;AAAA,UACX,QAAQ,MAAM;AAAA,UACd,WAAW;AAAA,UACX,WAAW,MAAM;AAAA,UACjB,UAAU;AAAA,UACV,QAAQ,MAAM;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAED,gBAAY,aAAa;AACzB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,iBAAiB,OAAe,gBAA6C;AACjF,QAAI,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAOZ,UAAM,SAAgB,CAAC,OAAO,KAAK,IAAI,CAAC;AAExC,QAAI,kBAAkB,eAAe,SAAS,GAAG;AAC/C,YAAM,eAAe,eAAe,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AAC3D,eAAS,0BAA0B,YAAY;AAC/C,aAAO,KAAK,GAAG,cAAc;AAAA,IAC/B;AAEA,aAAS;AAET,UAAM,OAAO,KAAK,GAAG,QAAQ,KAAK;AAClC,UAAM,OAAO,KAAK,IAAI,GAAG,MAAM;AAE/B,WAAO,QAAQ,IAAI,KAAK,IAAI,SAAO,KAAK,WAAW,GAAG,CAAC,CAAC;AAAA,EAC1D;AAAA,EAEA,MAAM,kBAAkB,QAAkC;AACxD,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B;AAED,UAAM,OAAO,KAAK,IAAI,QAAQ,KAAK,IAAI,CAAC;AACxC,WAAO,QAAQ,IAAI,KAAK,IAAI,SAAO,KAAK,WAAW,GAAG,CAAC,CAAC;AAAA,EAC1D;AAAA,EAEA,MAAM,aAAa,SAAwC;AACzD,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B;AAED,UAAM,MAAM,KAAK,IAAI,SAAS,KAAK,IAAI,CAAC;AAExC,QAAI,CAAC,KAAK;AACR,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,WAAW,GAAG;AAAA,EAC5B;AAAA,EAEA,MAAM,YAAY,SAAiB,aAAuC;AACxE,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B;AAED,UAAM,SAAS,KAAK,IAAI,SAAS,WAAW;AAC5C,WAAO,OAAO,UAAU;AAAA,EAC1B;AAAA,EAEA,MAAM,oBAAoB,KAA8B;AACtD,UAAM,OAAO,KAAK,GAAG,QAAQ,yCAAyC;AACtE,UAAM,SAAS,KAAK,IAAI,GAAG;AAC3B,WAAO,OAAO;AAAA,EAChB;AAAA,EAEA,MAAM,YACJ,SACA,gBACA,WACA,QAC+C;AAE/C,UAAM,QAAQ,MAAM,KAAK,aAAa,OAAO;AAE7C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,MAAM,UAAU,MAAM,WAAW,QAAQ;AAC3C,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,MAAM,gBAAgB;AACxB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAGA,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B;AAED,UAAM,SAAS,KAAK,IAAI,gBAAgB,WAAW,KAAK,IAAI,GAAG,OAAO;AAEtE,QAAI,OAAO,YAAY,GAAG;AACxB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AAAA,EAEA,MAAM,kBAAkB,eAAyC;AAC/D,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B;AAED,UAAM,OAAO,KAAK,IAAI,eAAe,KAAK,IAAI,CAAC;AAC/C,WAAO,QAAQ,IAAI,KAAK,IAAI,SAAO,KAAK,WAAW,GAAG,CAAC,CAAC;AAAA,EAC1D;AAAA,EAEA,MAAM,iBACJ,SACA,QACA,MACA,YACiB;AACjB,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B;AAED,UAAM,gBAAgB,KAAK,IAAI;AAC/B,UAAM,cAAc,KAAK,GAAG,YAAY,CAACC,gBAAsB;AAC7D,eAAS,IAAI,GAAG,IAAIA,YAAW,QAAQ,KAAK;AAC1C,aAAK;AAAA,UACH;AAAA,UACA;AAAA,UACA;AAAA,UACA,KAAK,UAAUA,YAAW,CAAC,CAAC;AAAA;AAAA,UAC5B,gBAAgB;AAAA;AAAA,QAClB;AAAA,MACF;AAAA,IACF,CAAC;AAED,gBAAY,UAAU;AACtB,WAAO,WAAW;AAAA,EACpB;AAAA,EAEA,MAAM,iBACJ,SACA,YACA,OACyB;AACzB,QAAI,QAAQ;AAAA;AAAA;AAAA;AAKZ,UAAM,SAAgB,CAAC,SAAS,UAAU;AAE1C,QAAI,UAAU,QAAW;AACvB,eAAS;AACT,aAAO,KAAK,KAAK;AAAA,IACnB;AAEA,aAAS;AAET,UAAM,OAAO,KAAK,GAAG,QAAQ,KAAK;AAClC,UAAM,OAAO,KAAK,IAAI,GAAG,MAAM;AAE/B,WAAO,KAAK,IAAI,UAAQ;AAAA,MACtB,IAAI,IAAI;AAAA,MACR,SAAS,IAAI;AAAA,MACb,QAAQ,IAAI;AAAA,MACZ,MAAM,IAAI;AAAA,MACV,WAAW,KAAK,MAAM,IAAI,SAAS;AAAA;AAAA,MACnC,WAAW,IAAI;AAAA,IACjB,EAAE;AAAA,EACJ;AAAA,EAEA,MAAM,UAAU,OAAe,QAAgB,YAG5C;AACD,UAAM,MAAM,KAAK,IAAI;AAGrB,UAAM,cAAc,aAChB,yCACA;AAEJ,UAAM,oBAAoB,aAAa,GAAG,UAAU,MAAM;AAG1D,UAAM,aAAa;AAAA;AAAA;AAAA;AAAA,cAIT,WAAW;AAAA;AAGrB,UAAM,YAAY,KAAK,GAAG,QAAQ,UAAU;AAC5C,UAAM,cAAc,aAAa,CAAC,KAAK,iBAAiB,IAAI,CAAC,GAAG;AAChE,UAAM,WAAW,UAAU,IAAI,GAAG,WAAW;AAC7C,UAAM,QAAQ,SAAS;AAGvB,UAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMV,WAAW;AAAA;AAAA;AAAA;AAAA;AAMrB,UAAM,aAAa,KAAK,GAAG,QAAQ,WAAW;AAC9C,UAAM,eAAe,aACjB,CAAC,KAAK,mBAAmB,OAAO,MAAM,IACtC,CAAC,KAAK,OAAO,MAAM;AACvB,UAAM,OAAO,WAAW,IAAI,GAAG,YAAY;AAE3C,UAAM,SAAS,KAAK,IAAI,UAAQ;AAAA,MAC9B,OAAO,IAAI;AAAA,MACX,aAAa,IAAI;AAAA,IACnB,EAAE;AAEF,WAAO,EAAE,QAAQ,MAAM;AAAA,EACzB;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,GAAG,MAAM;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,WAAW,KAA0B;AAEjD,UAAM,YAAY,KAAK,GAAG,QAAQ;AAAA;AAAA,KAEjC;AAED,UAAM,YAAY,UAAU,IAAI,IAAI,EAAE;AACtC,UAAM,SAAS,UAAU,IAAI,OAAK,EAAE,KAAK;AAEzC,WAAO;AAAA,MACL,IAAI,IAAI;AAAA,MACR,QAAQ,IAAI;AAAA,MACZ,KAAK,IAAI;AAAA,MACT;AAAA,MACA,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,MACf,UAAU,IAAI;AAAA,MACd,QAAQ,IAAI,UAAU;AAAA,MACtB,gBAAgB,IAAI,oBAAoB;AAAA,MACxC,WAAW,IAAI,cAAc;AAAA,MAC7B,YAAY,IAAI,eAAe;AAAA,IACjC;AAAA,EACF;AACF;;;ANzYA,eAAe,OAAO;AACpB,QAAM,SAAS,WAAW;AAE1B,UAAQ,IAAI,4BAA4B;AACxC,UAAQ,IAAI,kBAAkB;AAAA,IAC5B,MAAM,OAAO;AAAA,IACb,aAAa,OAAO;AAAA,IACpB,aAAa,OAAO;AAAA,IACpB,iBAAiB,GAAG,OAAO,eAAe;AAAA,IAC1C,aAAa,GAAG,OAAO,WAAW;AAAA,IAClC,aAAa,GAAG,OAAO,WAAW;AAAA,IAClC,iBAAiB,GAAG,OAAO,eAAe;AAAA,IAC1C,qBAAqB,OAAO;AAAA,IAC5B,mBAAmB,OAAO;AAAA,IAC1B,aAAa,OAAO;AAAA,IACpB,SAAS,OAAO;AAAA,EAClB,CAAC;AAED,MAAI;AAEJ,MAAI,OAAO,gBAAgB,UAAU;AACnC,cAAU,IAAI,cAAc,OAAO,WAAW;AAC9C,YAAQ,IAAI,sBAAsB;AAAA,EACpC,OAAO;AACL,UAAM,IAAI,MAAM,0BAA0B;AAAA,EAC5C;AAGA,QAAM,kBAAkB,YAAY,YAAY;AAC9C,QAAI;AACF,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,UAAU,MAAM,QAAQ,oBAAoB,GAAG;AACrD,UAAI,UAAU,GAAG;AACf,gBAAQ,IAAI,oBAAoB,OAAO,mBAAmB;AAAA,MAC5D;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,MAAM,kBAAkB,GAAG;AAAA,IACrC;AAAA,EACF,GAAG,OAAO,eAAe;AAEzB,QAAM,MAAM,UAAU,SAAS,MAAM;AAErC,QAAM,aAAS,0BAAM;AAAA,IACnB,OAAO,IAAI;AAAA,IACX,MAAM,OAAO;AAAA,EACf,CAAC;AAED,UAAQ,IAAI,sCAAsC,OAAO,IAAI,EAAE;AAC/D,UAAQ,IAAI,6BAA6B;AAGzC,QAAM,WAAW,YAAY;AAC3B,YAAQ,IAAI,+BAA+B;AAC3C,kBAAc,eAAe;AAC7B,UAAM,QAAQ,MAAM;AACpB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,GAAG,UAAU,QAAQ;AAC7B,UAAQ,GAAG,WAAW,QAAQ;AAChC;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,MAAM,gBAAgB,GAAG;AACjC,UAAQ,KAAK,CAAC;AAChB,CAAC;",
6
6
  "names": ["c", "Database", "offersWithIds", "candidates"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Topic-based peer discovery and signaling server for distributed P2P applications",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/app.ts CHANGED
@@ -64,11 +64,37 @@ export function createApp(storage: Storage, config: Config) {
64
64
  /**
65
65
  * POST /register
66
66
  * Register a new peer and receive credentials
67
+ * Accepts optional peerId in request body for custom peer IDs
67
68
  */
68
69
  app.post('/register', async (c) => {
69
70
  try {
70
- // Generate new peer ID
71
- const peerId = generatePeerId();
71
+ let peerId: string;
72
+
73
+ // Check if custom peer ID is provided
74
+ const body = await c.req.json().catch(() => ({}));
75
+ const customPeerId = body.peerId;
76
+
77
+ if (customPeerId !== undefined) {
78
+ // Validate custom peer ID
79
+ if (typeof customPeerId !== 'string' || customPeerId.length === 0) {
80
+ return c.json({ error: 'Peer ID must be a non-empty string' }, 400);
81
+ }
82
+
83
+ if (customPeerId.length > 128) {
84
+ return c.json({ error: 'Peer ID must be 128 characters or less' }, 400);
85
+ }
86
+
87
+ // Check if peer ID is already in use by checking for active offers
88
+ const existingOffers = await storage.getOffersByPeerId(customPeerId);
89
+ if (existingOffers.length > 0) {
90
+ return c.json({ error: 'Peer ID is already in use' }, 409);
91
+ }
92
+
93
+ peerId = customPeerId;
94
+ } else {
95
+ // Generate new peer ID
96
+ peerId = generatePeerId();
97
+ }
72
98
 
73
99
  // Encrypt peer ID with server secret (async operation)
74
100
  const secret = await encryptPeerId(peerId, config.authSecret);
package/API.md DELETED
@@ -1,458 +0,0 @@
1
- # HTTP API
2
-
3
- This API provides peer signaling and tracking endpoints for distributed peer-to-peer applications. Uses JSON request/response bodies with Origin-based session isolation.
4
-
5
- All endpoints require an `Origin` header and accept `application/json` content type.
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- Sessions are organized by:
12
- - **Origin**: The HTTP Origin header (e.g., `https://example.com`) - isolates sessions by application
13
- - **Topic**: A string identifier for grouping related peers (max 256 chars)
14
- - **Info**: User-provided metadata (max 1024 chars) to uniquely identify each peer
15
-
16
- This allows multiple peers from the same application (origin) to discover each other through topics while preventing duplicate connections by comparing the info field.
17
-
18
- ---
19
-
20
- ## GET `/`
21
-
22
- Returns server version information including the git commit hash used to build the server.
23
-
24
- ### Response
25
-
26
- **Content-Type:** `application/json`
27
-
28
- **Success (200 OK):**
29
- ```json
30
- {
31
- "version": "a1b2c3d"
32
- }
33
- ```
34
-
35
- **Notes:**
36
- - Returns the git commit hash from build time
37
- - Returns "unknown" if git information is not available
38
-
39
- ### Example
40
-
41
- ```bash
42
- curl -X GET http://localhost:3000/
43
- ```
44
-
45
- ---
46
-
47
- ## GET `/topics`
48
-
49
- Lists all topics with the count of available peers for each (paginated). Returns only topics that have unanswered sessions.
50
-
51
- ### Request
52
-
53
- **Headers:**
54
- - `Origin: https://example.com` (required)
55
-
56
- **Query Parameters:**
57
-
58
- | Parameter | Type | Required | Default | Description |
59
- |-----------|--------|----------|---------|---------------------------------|
60
- | `page` | number | No | `1` | Page number (starting from 1) |
61
- | `limit` | number | No | `100` | Results per page (max 1000) |
62
-
63
- ### Response
64
-
65
- **Content-Type:** `application/json`
66
-
67
- **Success (200 OK):**
68
- ```json
69
- {
70
- "topics": [
71
- {
72
- "topic": "my-room",
73
- "count": 3
74
- },
75
- {
76
- "topic": "another-room",
77
- "count": 1
78
- }
79
- ],
80
- "pagination": {
81
- "page": 1,
82
- "limit": 100,
83
- "total": 2,
84
- "hasMore": false
85
- }
86
- }
87
- ```
88
-
89
- **Notes:**
90
- - Only returns topics from the same origin as the request
91
- - Only includes topics with at least one unanswered session
92
- - Topics are sorted alphabetically
93
- - Counts only include unexpired sessions
94
- - Maximum 1000 results per page
95
-
96
- ### Examples
97
-
98
- **Default pagination (page 1, limit 100):**
99
- ```bash
100
- curl -X GET http://localhost:3000/topics \
101
- -H "Origin: https://example.com"
102
- ```
103
-
104
- **Custom pagination:**
105
- ```bash
106
- curl -X GET "http://localhost:3000/topics?page=2&limit=50" \
107
- -H "Origin: https://example.com"
108
- ```
109
-
110
- ---
111
-
112
- ## GET `/:topic/sessions`
113
-
114
- Discovers available peers for a given topic. Returns all unanswered sessions from the requesting origin.
115
-
116
- ### Request
117
-
118
- **Headers:**
119
- - `Origin: https://example.com` (required)
120
-
121
- **Path Parameters:**
122
-
123
- | Parameter | Type | Required | Description |
124
- |-----------|--------|----------|-------------------------------|
125
- | `topic` | string | Yes | Topic identifier to query |
126
-
127
- ### Response
128
-
129
- **Content-Type:** `application/json`
130
-
131
- **Success (200 OK):**
132
- ```json
133
- {
134
- "sessions": [
135
- {
136
- "code": "550e8400-e29b-41d4-a716-446655440000",
137
- "info": "peer-123",
138
- "offer": "<SIGNALING_DATA>",
139
- "offerCandidates": ["<SIGNALING_DATA>"],
140
- "createdAt": 1699564800000,
141
- "expiresAt": 1699565100000
142
- },
143
- {
144
- "code": "660e8400-e29b-41d4-a716-446655440001",
145
- "info": "peer-456",
146
- "offer": "<SIGNALING_DATA>",
147
- "offerCandidates": [],
148
- "createdAt": 1699564850000,
149
- "expiresAt": 1699565150000
150
- }
151
- ]
152
- }
153
- ```
154
-
155
- **Notes:**
156
- - Only returns sessions from the same origin as the request
157
- - Only returns sessions that haven't been answered yet
158
- - Sessions are ordered by creation time (newest first)
159
- - Use the `info` field to avoid answering your own offers
160
-
161
- ### Example
162
-
163
- ```bash
164
- curl -X GET http://localhost:3000/my-room/sessions \
165
- -H "Origin: https://example.com"
166
- ```
167
-
168
- ---
169
-
170
- ## POST `/:topic/offer`
171
-
172
- Announces peer availability and creates a new session for the specified topic. Returns a unique session code (UUID) for other peers to connect to.
173
-
174
- ### Request
175
-
176
- **Headers:**
177
- - `Content-Type: application/json`
178
- - `Origin: https://example.com` (required)
179
-
180
- **Path Parameters:**
181
-
182
- | Parameter | Type | Required | Description |
183
- |-----------|--------|----------|----------------------------------------------|
184
- | `topic` | string | Yes | Topic identifier for grouping peers (max 256 characters) |
185
-
186
- **Body Parameters:**
187
-
188
- | Parameter | Type | Required | Description |
189
- |-----------|--------|----------|----------------------------------------------|
190
- | `info` | string | Yes | Peer identifier/metadata (max 1024 characters) |
191
- | `offer` | string | Yes | Signaling data for peer connection |
192
-
193
- ### Response
194
-
195
- **Content-Type:** `application/json`
196
-
197
- **Success (200 OK):**
198
- ```json
199
- {
200
- "code": "550e8400-e29b-41d4-a716-446655440000"
201
- }
202
- ```
203
-
204
- Returns a unique UUID session code.
205
-
206
- ### Example
207
-
208
- ```bash
209
- curl -X POST http://localhost:3000/my-room/offer \
210
- -H "Content-Type: application/json" \
211
- -H "Origin: https://example.com" \
212
- -d '{
213
- "info": "peer-123",
214
- "offer": "<SIGNALING_DATA>"
215
- }'
216
-
217
- # Response:
218
- # {"code":"550e8400-e29b-41d4-a716-446655440000"}
219
- ```
220
-
221
- ---
222
-
223
- ## POST `/answer`
224
-
225
- Connects to an existing peer session by sending connection data or exchanging signaling information.
226
-
227
- ### Request
228
-
229
- **Headers:**
230
- - `Content-Type: application/json`
231
- - `Origin: https://example.com` (required)
232
-
233
- **Body Parameters:**
234
-
235
- | Parameter | Type | Required | Description |
236
- |-------------|--------|----------|----------------------------------------------------------|
237
- | `code` | string | Yes | The session UUID from the offer |
238
- | `answer` | string | No* | Response signaling data for connection establishment |
239
- | `candidate` | string | No* | Additional signaling data for connection negotiation |
240
- | `side` | string | Yes | Which peer is sending: `offerer` or `answerer` |
241
-
242
- *Either `answer` or `candidate` must be provided, but not both.
243
-
244
- ### Response
245
-
246
- **Content-Type:** `application/json`
247
-
248
- **Success (200 OK):**
249
- ```json
250
- {
251
- "success": true
252
- }
253
- ```
254
-
255
- **Notes:**
256
- - Origin header must match the session's origin
257
- - Sessions are isolated by origin to group topics by domain
258
-
259
- ### Examples
260
-
261
- **Sending connection response:**
262
- ```bash
263
- curl -X POST http://localhost:3000/answer \
264
- -H "Content-Type: application/json" \
265
- -H "Origin: https://example.com" \
266
- -d '{
267
- "code": "550e8400-e29b-41d4-a716-446655440000",
268
- "answer": "<SIGNALING_DATA>",
269
- "side": "answerer"
270
- }'
271
-
272
- # Response:
273
- # {"success":true}
274
- ```
275
-
276
- **Sending additional signaling data:**
277
- ```bash
278
- curl -X POST http://localhost:3000/answer \
279
- -H "Content-Type: application/json" \
280
- -H "Origin: https://example.com" \
281
- -d '{
282
- "code": "550e8400-e29b-41d4-a716-446655440000",
283
- "candidate": "<SIGNALING_DATA>",
284
- "side": "offerer"
285
- }'
286
-
287
- # Response:
288
- # {"success":true}
289
- ```
290
-
291
- ---
292
-
293
- ## POST `/poll`
294
-
295
- Retrieves session data including offers, responses, and signaling information from the other peer.
296
-
297
- ### Request
298
-
299
- **Headers:**
300
- - `Content-Type: application/json`
301
- - `Origin: https://example.com` (required)
302
-
303
- **Body Parameters:**
304
-
305
- | Parameter | Type | Required | Description |
306
- |-----------|--------|----------|-------------------------------------------------|
307
- | `code` | string | Yes | The session UUID |
308
- | `side` | string | Yes | Which side is polling: `offerer` or `answerer` |
309
-
310
- ### Response
311
-
312
- **Content-Type:** `application/json`
313
-
314
- **Success (200 OK):**
315
-
316
- Response varies by side:
317
-
318
- **For `side=offerer` (the offerer polls for response from answerer):**
319
- ```json
320
- {
321
- "answer": "<SIGNALING_DATA>",
322
- "answerCandidates": [
323
- "<SIGNALING_DATA_1>",
324
- "<SIGNALING_DATA_2>"
325
- ]
326
- }
327
- ```
328
-
329
- **For `side=answerer` (the answerer polls for offer from offerer):**
330
- ```json
331
- {
332
- "offer": "<SIGNALING_DATA>",
333
- "offerCandidates": [
334
- "<SIGNALING_DATA_1>",
335
- "<SIGNALING_DATA_2>"
336
- ]
337
- }
338
- ```
339
-
340
- **Notes:**
341
- - `answer` will be `null` if the answerer hasn't responded yet
342
- - Candidate arrays will be empty `[]` if no additional signaling data has been sent
343
- - Use this endpoint for polling to check for new signaling data
344
- - Origin header must match the session's origin
345
-
346
- ### Examples
347
-
348
- **Answerer polling for signaling data:**
349
- ```bash
350
- curl -X POST http://localhost:3000/poll \
351
- -H "Content-Type: application/json" \
352
- -H "Origin: https://example.com" \
353
- -d '{
354
- "code": "550e8400-e29b-41d4-a716-446655440000",
355
- "side": "answerer"
356
- }'
357
-
358
- # Response:
359
- # {
360
- # "offer": "<SIGNALING_DATA>",
361
- # "offerCandidates": ["<SIGNALING_DATA>"]
362
- # }
363
- ```
364
-
365
- **Offerer polling for response:**
366
- ```bash
367
- curl -X POST http://localhost:3000/poll \
368
- -H "Content-Type: application/json" \
369
- -H "Origin: https://example.com" \
370
- -d '{
371
- "code": "550e8400-e29b-41d4-a716-446655440000",
372
- "side": "offerer"
373
- }'
374
-
375
- # Response:
376
- # {
377
- # "answer": "<SIGNALING_DATA>",
378
- # "answerCandidates": ["<SIGNALING_DATA>"]
379
- # }
380
- ```
381
-
382
- ---
383
-
384
- ## GET `/health`
385
-
386
- Health check endpoint.
387
-
388
- ### Response
389
-
390
- **Content-Type:** `application/json`
391
-
392
- **Success (200 OK):**
393
- ```json
394
- {
395
- "status": "ok",
396
- "timestamp": 1699564800000
397
- }
398
- ```
399
-
400
- ---
401
-
402
- ## Error Responses
403
-
404
- All endpoints may return the following error responses:
405
-
406
- **400 Bad Request:**
407
- ```json
408
- {
409
- "error": "Missing or invalid required parameter: topic"
410
- }
411
- ```
412
-
413
- **404 Not Found:**
414
- ```json
415
- {
416
- "error": "Session not found, expired, or origin mismatch"
417
- }
418
- ```
419
-
420
- **500 Internal Server Error:**
421
- ```json
422
- {
423
- "error": "Internal server error"
424
- }
425
- ```
426
-
427
- ---
428
-
429
- ## Usage Flow
430
-
431
- ### Peer Discovery and Connection
432
-
433
- 1. **Check server version (optional):**
434
- - GET `/` to see server version information
435
-
436
- 2. **Discover active topics:**
437
- - GET `/topics` to see all topics and peer counts
438
- - Optional: paginate through results with `?page=2&limit=100`
439
-
440
- 3. **Peer A announces availability:**
441
- - POST `/:topic/offer` with peer identifier and signaling data
442
- - Receives a unique session code
443
-
444
- 4. **Peer B discovers peers:**
445
- - GET `/:topic/sessions` to list available sessions in a topic
446
- - Filters out sessions with their own info to avoid self-connection
447
- - Selects a peer to connect to
448
-
449
- 5. **Peer B initiates connection:**
450
- - POST `/answer` with the session code and their signaling data
451
-
452
- 6. **Both peers exchange signaling information:**
453
- - POST `/answer` with additional signaling data as needed
454
- - POST `/poll` to retrieve signaling data from the other peer
455
-
456
- 7. **Peer connection established**
457
- - Peers use exchanged signaling data to establish direct connection
458
- - Session automatically expires after configured timeout