@xtr-dev/rondevu-server 0.0.1 → 0.1.1
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/API.md +39 -9
- package/CLAUDE.md +47 -0
- package/README.md +144 -187
- package/build.js +12 -0
- package/dist/index.js +799 -266
- package/dist/index.js.map +4 -4
- package/migrations/0001_add_peer_id.sql +21 -0
- package/migrations/0002_remove_topics.sql +22 -0
- package/migrations/0003_remove_origin.sql +29 -0
- package/migrations/0004_add_secret.sql +4 -0
- package/migrations/schema.sql +18 -0
- package/package.json +4 -3
- package/src/app.ts +421 -127
- package/src/bloom.ts +66 -0
- package/src/config.ts +27 -2
- package/src/crypto.ts +149 -0
- package/src/index.ts +28 -12
- package/src/middleware/auth.ts +51 -0
- package/src/storage/d1.ts +394 -0
- package/src/storage/hash-id.ts +37 -0
- package/src/storage/sqlite.ts +323 -178
- package/src/storage/types.ts +128 -54
- package/src/worker.ts +51 -16
- package/wrangler.toml +45 -0
- package/DEPLOYMENT.md +0 -346
- package/src/storage/kv.ts +0 -241
package/src/storage/types.ts
CHANGED
|
@@ -1,87 +1,161 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Represents a WebRTC signaling
|
|
2
|
+
* Represents a WebRTC signaling offer with topic-based discovery
|
|
3
3
|
*/
|
|
4
|
-
export interface
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
offer: string;
|
|
10
|
-
answer?: string;
|
|
11
|
-
offerCandidates: string[];
|
|
12
|
-
answerCandidates: string[];
|
|
4
|
+
export interface Offer {
|
|
5
|
+
id: string;
|
|
6
|
+
peerId: string;
|
|
7
|
+
sdp: string;
|
|
8
|
+
topics: string[];
|
|
13
9
|
createdAt: number;
|
|
14
10
|
expiresAt: number;
|
|
11
|
+
lastSeen: number;
|
|
12
|
+
secret?: string;
|
|
13
|
+
answererPeerId?: string;
|
|
14
|
+
answerSdp?: string;
|
|
15
|
+
answeredAt?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents an ICE candidate for WebRTC signaling
|
|
20
|
+
* Stores the complete candidate object as plain JSON (no type enforcement)
|
|
21
|
+
*/
|
|
22
|
+
export interface IceCandidate {
|
|
23
|
+
id: number;
|
|
24
|
+
offerId: string;
|
|
25
|
+
peerId: string;
|
|
26
|
+
role: 'offerer' | 'answerer';
|
|
27
|
+
candidate: any; // Full candidate object as JSON - don't enforce structure
|
|
28
|
+
createdAt: number;
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
32
|
+
* Represents a topic with active peer count
|
|
33
|
+
*/
|
|
34
|
+
export interface TopicInfo {
|
|
35
|
+
topic: string;
|
|
36
|
+
activePeers: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Request to create a new offer
|
|
41
|
+
*/
|
|
42
|
+
export interface CreateOfferRequest {
|
|
43
|
+
id?: string;
|
|
44
|
+
peerId: string;
|
|
45
|
+
sdp: string;
|
|
46
|
+
topics: string[];
|
|
47
|
+
expiresAt: number;
|
|
48
|
+
secret?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Storage interface for offer management with topic-based discovery
|
|
53
|
+
* Implementations can use different backends (SQLite, D1, Memory, etc.)
|
|
20
54
|
*/
|
|
21
55
|
export interface Storage {
|
|
22
56
|
/**
|
|
23
|
-
* Creates
|
|
24
|
-
* @param
|
|
25
|
-
* @
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
57
|
+
* Creates one or more offers
|
|
58
|
+
* @param offers Array of offer creation requests
|
|
59
|
+
* @returns Array of created offers with IDs
|
|
60
|
+
*/
|
|
61
|
+
createOffers(offers: CreateOfferRequest[]): Promise<Offer[]>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retrieves offers by topic with optional peer ID exclusion
|
|
65
|
+
* @param topic Topic to search for
|
|
66
|
+
* @param excludePeerIds Optional array of peer IDs to exclude
|
|
67
|
+
* @returns Array of offers matching the topic
|
|
68
|
+
*/
|
|
69
|
+
getOffersByTopic(topic: string, excludePeerIds?: string[]): Promise<Offer[]>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Retrieves all offers from a specific peer
|
|
73
|
+
* @param peerId Peer identifier
|
|
74
|
+
* @returns Array of offers from the peer
|
|
30
75
|
*/
|
|
31
|
-
|
|
76
|
+
getOffersByPeerId(peerId: string): Promise<Offer[]>;
|
|
32
77
|
|
|
33
78
|
/**
|
|
34
|
-
*
|
|
35
|
-
* @param
|
|
36
|
-
* @
|
|
37
|
-
* @returns Array of sessions that haven't been answered yet
|
|
79
|
+
* Retrieves a specific offer by ID
|
|
80
|
+
* @param offerId Offer identifier
|
|
81
|
+
* @returns The offer if found, null otherwise
|
|
38
82
|
*/
|
|
39
|
-
|
|
83
|
+
getOfferById(offerId: string): Promise<Offer | null>;
|
|
40
84
|
|
|
41
85
|
/**
|
|
42
|
-
*
|
|
43
|
-
* @param
|
|
44
|
-
* @param
|
|
45
|
-
* @
|
|
46
|
-
* @returns Object with topics array and pagination metadata
|
|
86
|
+
* Deletes an offer (with ownership verification)
|
|
87
|
+
* @param offerId Offer identifier
|
|
88
|
+
* @param ownerPeerId Peer ID of the owner (for verification)
|
|
89
|
+
* @returns true if deleted, false if not found or not owned
|
|
47
90
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
deleteOffer(offerId: string, ownerPeerId: string): Promise<boolean>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Deletes all expired offers
|
|
95
|
+
* @param now Current timestamp
|
|
96
|
+
* @returns Number of offers deleted
|
|
97
|
+
*/
|
|
98
|
+
deleteExpiredOffers(now: number): Promise<number>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Answers an offer (locks it to the answerer)
|
|
102
|
+
* @param offerId Offer identifier
|
|
103
|
+
* @param answererPeerId Answerer's peer ID
|
|
104
|
+
* @param answerSdp WebRTC answer SDP
|
|
105
|
+
* @param secret Optional secret for protected offers
|
|
106
|
+
* @returns Success status and optional error message
|
|
107
|
+
*/
|
|
108
|
+
answerOffer(offerId: string, answererPeerId: string, answerSdp: string, secret?: string): Promise<{
|
|
109
|
+
success: boolean;
|
|
110
|
+
error?: string;
|
|
56
111
|
}>;
|
|
57
112
|
|
|
58
113
|
/**
|
|
59
|
-
* Retrieves
|
|
60
|
-
* @param
|
|
61
|
-
* @
|
|
62
|
-
* @returns The session if found, null otherwise
|
|
114
|
+
* Retrieves all answered offers for a specific offerer
|
|
115
|
+
* @param offererPeerId Offerer's peer ID
|
|
116
|
+
* @returns Array of answered offers
|
|
63
117
|
*/
|
|
64
|
-
|
|
118
|
+
getAnsweredOffers(offererPeerId: string): Promise<Offer[]>;
|
|
65
119
|
|
|
66
120
|
/**
|
|
67
|
-
*
|
|
68
|
-
* @param
|
|
69
|
-
* @param
|
|
70
|
-
* @param
|
|
121
|
+
* Adds ICE candidates for an offer
|
|
122
|
+
* @param offerId Offer identifier
|
|
123
|
+
* @param peerId Peer ID posting the candidates
|
|
124
|
+
* @param role Role of the peer (offerer or answerer)
|
|
125
|
+
* @param candidates Array of candidate objects (stored as plain JSON)
|
|
126
|
+
* @returns Number of candidates added
|
|
71
127
|
*/
|
|
72
|
-
|
|
128
|
+
addIceCandidates(
|
|
129
|
+
offerId: string,
|
|
130
|
+
peerId: string,
|
|
131
|
+
role: 'offerer' | 'answerer',
|
|
132
|
+
candidates: any[]
|
|
133
|
+
): Promise<number>;
|
|
73
134
|
|
|
74
135
|
/**
|
|
75
|
-
*
|
|
76
|
-
* @param
|
|
136
|
+
* Retrieves ICE candidates for an offer
|
|
137
|
+
* @param offerId Offer identifier
|
|
138
|
+
* @param targetRole Role to retrieve candidates for (offerer or answerer)
|
|
139
|
+
* @param since Optional timestamp - only return candidates after this time
|
|
140
|
+
* @returns Array of ICE candidates
|
|
77
141
|
*/
|
|
78
|
-
|
|
142
|
+
getIceCandidates(
|
|
143
|
+
offerId: string,
|
|
144
|
+
targetRole: 'offerer' | 'answerer',
|
|
145
|
+
since?: number
|
|
146
|
+
): Promise<IceCandidate[]>;
|
|
79
147
|
|
|
80
148
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
149
|
+
* Retrieves topics with active peer counts (paginated)
|
|
150
|
+
* @param limit Maximum number of topics to return
|
|
151
|
+
* @param offset Number of topics to skip
|
|
152
|
+
* @param startsWith Optional prefix filter - only return topics starting with this string
|
|
153
|
+
* @returns Object with topics array and total count
|
|
83
154
|
*/
|
|
84
|
-
|
|
155
|
+
getTopics(limit: number, offset: number, startsWith?: string): Promise<{
|
|
156
|
+
topics: TopicInfo[];
|
|
157
|
+
total: number;
|
|
158
|
+
}>;
|
|
85
159
|
|
|
86
160
|
/**
|
|
87
161
|
* Closes the storage connection and releases resources
|
package/src/worker.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { createApp } from './app.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { D1Storage } from './storage/d1.ts';
|
|
3
|
+
import { generateSecretKey } from './crypto.ts';
|
|
4
|
+
import { Config } from './config.ts';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Cloudflare Workers environment bindings
|
|
6
8
|
*/
|
|
7
9
|
export interface Env {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
DB: D1Database;
|
|
11
|
+
AUTH_SECRET?: string;
|
|
12
|
+
OFFER_DEFAULT_TTL?: string;
|
|
13
|
+
OFFER_MAX_TTL?: string;
|
|
14
|
+
OFFER_MIN_TTL?: string;
|
|
15
|
+
MAX_OFFERS_PER_REQUEST?: string;
|
|
16
|
+
MAX_TOPICS_PER_OFFER?: string;
|
|
10
17
|
CORS_ORIGINS?: string;
|
|
18
|
+
VERSION?: string;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -15,25 +23,52 @@ export interface Env {
|
|
|
15
23
|
*/
|
|
16
24
|
export default {
|
|
17
25
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
18
|
-
// Initialize
|
|
19
|
-
const storage = new
|
|
26
|
+
// Initialize D1 storage
|
|
27
|
+
const storage = new D1Storage(env.DB);
|
|
20
28
|
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
? parseInt(env.SESSION_TIMEOUT, 10)
|
|
24
|
-
: 300000; // 5 minutes default
|
|
29
|
+
// Generate or use provided auth secret
|
|
30
|
+
const authSecret = env.AUTH_SECRET || generateSecretKey();
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
:
|
|
32
|
+
// Build config from environment
|
|
33
|
+
const config: Config = {
|
|
34
|
+
port: 0, // Not used in Workers
|
|
35
|
+
storageType: 'sqlite', // D1 is SQLite-compatible
|
|
36
|
+
storagePath: '', // Not used with D1
|
|
37
|
+
corsOrigins: env.CORS_ORIGINS
|
|
38
|
+
? env.CORS_ORIGINS.split(',').map(o => o.trim())
|
|
39
|
+
: ['*'],
|
|
40
|
+
version: env.VERSION || 'unknown',
|
|
41
|
+
authSecret,
|
|
42
|
+
offerDefaultTtl: env.OFFER_DEFAULT_TTL ? parseInt(env.OFFER_DEFAULT_TTL, 10) : 60000,
|
|
43
|
+
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
|
|
44
|
+
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
|
|
45
|
+
cleanupInterval: 60000, // Not used in Workers (scheduled handler instead)
|
|
46
|
+
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100,
|
|
47
|
+
maxTopicsPerOffer: env.MAX_TOPICS_PER_OFFER ? parseInt(env.MAX_TOPICS_PER_OFFER, 10) : 50,
|
|
48
|
+
};
|
|
29
49
|
|
|
30
50
|
// Create Hono app
|
|
31
|
-
const app = createApp(storage,
|
|
32
|
-
sessionTimeout,
|
|
33
|
-
corsOrigins,
|
|
34
|
-
});
|
|
51
|
+
const app = createApp(storage, config);
|
|
35
52
|
|
|
36
53
|
// Handle request
|
|
37
54
|
return app.fetch(request, env, ctx);
|
|
38
55
|
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Scheduled handler for cron triggers
|
|
59
|
+
* Runs periodically to clean up expired offers
|
|
60
|
+
*/
|
|
61
|
+
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
62
|
+
const storage = new D1Storage(env.DB);
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Delete expired offers
|
|
67
|
+
const deletedCount = await storage.deleteExpiredOffers(now);
|
|
68
|
+
|
|
69
|
+
console.log(`Cleaned up ${deletedCount} expired offers at ${new Date(now).toISOString()}`);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error cleaning up offers:', error);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
39
74
|
};
|
package/wrangler.toml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name = "rondevu"
|
|
2
|
+
main = "src/worker.ts"
|
|
3
|
+
compatibility_date = "2024-01-01"
|
|
4
|
+
compatibility_flags = ["nodejs_compat"]
|
|
5
|
+
|
|
6
|
+
# D1 Database binding
|
|
7
|
+
[[d1_databases]]
|
|
8
|
+
binding = "DB"
|
|
9
|
+
database_name = "rondevu-offers"
|
|
10
|
+
database_id = "b94e3f71-816d-455b-a89d-927fa49532d0"
|
|
11
|
+
|
|
12
|
+
# Environment variables
|
|
13
|
+
[vars]
|
|
14
|
+
OFFER_DEFAULT_TTL = "60000" # Default offer TTL: 1 minute
|
|
15
|
+
OFFER_MAX_TTL = "86400000" # Max offer TTL: 24 hours
|
|
16
|
+
OFFER_MIN_TTL = "60000" # Min offer TTL: 1 minute
|
|
17
|
+
MAX_OFFERS_PER_REQUEST = "100" # Max offers per request
|
|
18
|
+
MAX_TOPICS_PER_OFFER = "50" # Max topics per offer
|
|
19
|
+
CORS_ORIGINS = "*" # Comma-separated list of allowed origins
|
|
20
|
+
VERSION = "0.1.0" # Semantic version
|
|
21
|
+
|
|
22
|
+
# AUTH_SECRET should be set as a secret, not a var
|
|
23
|
+
# Run: npx wrangler secret put AUTH_SECRET
|
|
24
|
+
# Enter a 64-character hex string (32 bytes)
|
|
25
|
+
|
|
26
|
+
# Build configuration
|
|
27
|
+
[build]
|
|
28
|
+
command = ""
|
|
29
|
+
|
|
30
|
+
# For local development:
|
|
31
|
+
# Run: npx wrangler dev
|
|
32
|
+
# The local D1 database will be created automatically
|
|
33
|
+
|
|
34
|
+
# For production deployment:
|
|
35
|
+
# 1. Create D1 database: npx wrangler d1 create rondevu-sessions
|
|
36
|
+
# 2. Update the 'database_id' field above with the returned ID
|
|
37
|
+
# 3. Initialize schema: npx wrangler d1 execute rondevu-sessions --remote --file=./migrations/schema.sql
|
|
38
|
+
# 4. Deploy: npx wrangler deploy
|
|
39
|
+
|
|
40
|
+
[observability]
|
|
41
|
+
[observability.logs]
|
|
42
|
+
enabled = false
|
|
43
|
+
head_sampling_rate = 1
|
|
44
|
+
invocation_logs = true
|
|
45
|
+
persist = true
|
package/DEPLOYMENT.md
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
# Deployment Guide
|
|
2
|
-
|
|
3
|
-
This guide covers deploying Rondevu to various platforms.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Cloudflare Workers](#cloudflare-workers)
|
|
8
|
-
- [Docker](#docker)
|
|
9
|
-
- [Node.js](#nodejs)
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Cloudflare Workers
|
|
14
|
-
|
|
15
|
-
Deploy to Cloudflare's edge network using Cloudflare Workers and KV storage.
|
|
16
|
-
|
|
17
|
-
### Prerequisites
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm install -g wrangler
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Setup
|
|
24
|
-
|
|
25
|
-
1. **Login to Cloudflare**
|
|
26
|
-
```bash
|
|
27
|
-
wrangler login
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
2. **Create KV Namespace**
|
|
31
|
-
```bash
|
|
32
|
-
# For production
|
|
33
|
-
wrangler kv:namespace create SESSIONS
|
|
34
|
-
|
|
35
|
-
# This will output something like:
|
|
36
|
-
# { binding = "SESSIONS", id = "abc123..." }
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
3. **Update wrangler.toml**
|
|
40
|
-
|
|
41
|
-
Edit `wrangler.toml` and replace `YOUR_KV_NAMESPACE_ID` with the ID from step 2:
|
|
42
|
-
|
|
43
|
-
```toml
|
|
44
|
-
[[kv_namespaces]]
|
|
45
|
-
binding = "SESSIONS"
|
|
46
|
-
id = "abc123..." # Your actual KV namespace ID
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
4. **Configure Environment Variables** (Optional)
|
|
50
|
-
|
|
51
|
-
Update `wrangler.toml` to customize settings:
|
|
52
|
-
|
|
53
|
-
```toml
|
|
54
|
-
[vars]
|
|
55
|
-
SESSION_TIMEOUT = "300000" # Session timeout in milliseconds
|
|
56
|
-
CORS_ORIGINS = "https://example.com,https://app.example.com"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Local Development
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
# Run locally with Wrangler
|
|
63
|
-
npx wrangler dev
|
|
64
|
-
|
|
65
|
-
# The local development server will:
|
|
66
|
-
# - Start on http://localhost:8787
|
|
67
|
-
# - Use a local KV namespace automatically
|
|
68
|
-
# - Hot-reload on file changes
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Production Deployment
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
# Deploy to Cloudflare Workers
|
|
75
|
-
npx wrangler deploy
|
|
76
|
-
|
|
77
|
-
# This will output your worker URL:
|
|
78
|
-
# https://rondevu.YOUR_SUBDOMAIN.workers.dev
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Custom Domain (Optional)
|
|
82
|
-
|
|
83
|
-
1. Go to your Cloudflare Workers dashboard
|
|
84
|
-
2. Select your worker
|
|
85
|
-
3. Click "Triggers" → "Add Custom Domain"
|
|
86
|
-
4. Enter your domain (e.g., `api.example.com`)
|
|
87
|
-
|
|
88
|
-
### Monitoring
|
|
89
|
-
|
|
90
|
-
View logs and analytics:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
# Stream real-time logs
|
|
94
|
-
npx wrangler tail
|
|
95
|
-
|
|
96
|
-
# View in dashboard
|
|
97
|
-
# Visit: https://dash.cloudflare.com → Workers & Pages
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Environment Variables
|
|
101
|
-
|
|
102
|
-
| Variable | Default | Description |
|
|
103
|
-
|----------|---------|-------------|
|
|
104
|
-
| `SESSION_TIMEOUT` | `300000` | Session timeout in milliseconds |
|
|
105
|
-
| `CORS_ORIGINS` | `*` | Comma-separated allowed origins |
|
|
106
|
-
|
|
107
|
-
### Pricing
|
|
108
|
-
|
|
109
|
-
Cloudflare Workers Free Tier includes:
|
|
110
|
-
- 100,000 requests/day
|
|
111
|
-
- 10ms CPU time per request
|
|
112
|
-
- KV: 100,000 reads/day, 1,000 writes/day
|
|
113
|
-
|
|
114
|
-
For higher usage, see [Cloudflare Workers pricing](https://workers.cloudflare.com/#plans).
|
|
115
|
-
|
|
116
|
-
### Advantages
|
|
117
|
-
|
|
118
|
-
- **Global Edge Network**: Deploy to 300+ locations worldwide
|
|
119
|
-
- **Instant Scaling**: Handles traffic spikes automatically
|
|
120
|
-
- **Low Latency**: Runs close to your users
|
|
121
|
-
- **No Server Management**: Fully serverless
|
|
122
|
-
- **Free Tier**: Generous limits for small projects
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## Docker
|
|
127
|
-
|
|
128
|
-
### Quick Start
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
# Build
|
|
132
|
-
docker build -t rondevu .
|
|
133
|
-
|
|
134
|
-
# Run with in-memory SQLite
|
|
135
|
-
docker run -p 3000:3000 -e STORAGE_PATH=:memory: rondevu
|
|
136
|
-
|
|
137
|
-
# Run with persistent SQLite
|
|
138
|
-
docker run -p 3000:3000 \
|
|
139
|
-
-v $(pwd)/data:/app/data \
|
|
140
|
-
-e STORAGE_PATH=/app/data/sessions.db \
|
|
141
|
-
rondevu
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Docker Compose
|
|
145
|
-
|
|
146
|
-
Create a `docker-compose.yml`:
|
|
147
|
-
|
|
148
|
-
```yaml
|
|
149
|
-
version: '3.8'
|
|
150
|
-
|
|
151
|
-
services:
|
|
152
|
-
rondevu:
|
|
153
|
-
build: .
|
|
154
|
-
ports:
|
|
155
|
-
- "3000:3000"
|
|
156
|
-
environment:
|
|
157
|
-
- PORT=3000
|
|
158
|
-
- STORAGE_TYPE=sqlite
|
|
159
|
-
- STORAGE_PATH=/app/data/sessions.db
|
|
160
|
-
- SESSION_TIMEOUT=300000
|
|
161
|
-
- CORS_ORIGINS=*
|
|
162
|
-
volumes:
|
|
163
|
-
- ./data:/app/data
|
|
164
|
-
restart: unless-stopped
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Run with:
|
|
168
|
-
```bash
|
|
169
|
-
docker-compose up -d
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Environment Variables
|
|
173
|
-
|
|
174
|
-
| Variable | Default | Description |
|
|
175
|
-
|----------|---------|-------------|
|
|
176
|
-
| `PORT` | `3000` | Server port |
|
|
177
|
-
| `STORAGE_TYPE` | `sqlite` | Storage backend |
|
|
178
|
-
| `STORAGE_PATH` | `/app/data/sessions.db` | SQLite database path |
|
|
179
|
-
| `SESSION_TIMEOUT` | `300000` | Session timeout in ms |
|
|
180
|
-
| `CORS_ORIGINS` | `*` | Allowed CORS origins |
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## Node.js
|
|
185
|
-
|
|
186
|
-
### Production Deployment
|
|
187
|
-
|
|
188
|
-
1. **Install Dependencies**
|
|
189
|
-
```bash
|
|
190
|
-
npm ci --production
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
2. **Build TypeScript**
|
|
194
|
-
```bash
|
|
195
|
-
npm run build
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
3. **Set Environment Variables**
|
|
199
|
-
```bash
|
|
200
|
-
export PORT=3000
|
|
201
|
-
export STORAGE_TYPE=sqlite
|
|
202
|
-
export STORAGE_PATH=./data/sessions.db
|
|
203
|
-
export SESSION_TIMEOUT=300000
|
|
204
|
-
export CORS_ORIGINS=*
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
4. **Run**
|
|
208
|
-
```bash
|
|
209
|
-
npm start
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### Process Manager (PM2)
|
|
213
|
-
|
|
214
|
-
For production, use a process manager like PM2:
|
|
215
|
-
|
|
216
|
-
1. **Install PM2**
|
|
217
|
-
```bash
|
|
218
|
-
npm install -g pm2
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
2. **Create ecosystem.config.js**
|
|
222
|
-
```javascript
|
|
223
|
-
module.exports = {
|
|
224
|
-
apps: [{
|
|
225
|
-
name: 'rondevu',
|
|
226
|
-
script: './dist/index.js',
|
|
227
|
-
instances: 'max',
|
|
228
|
-
exec_mode: 'cluster',
|
|
229
|
-
env: {
|
|
230
|
-
NODE_ENV: 'production',
|
|
231
|
-
PORT: 3000,
|
|
232
|
-
STORAGE_TYPE: 'sqlite',
|
|
233
|
-
STORAGE_PATH: './data/sessions.db',
|
|
234
|
-
SESSION_TIMEOUT: 300000,
|
|
235
|
-
CORS_ORIGINS: '*'
|
|
236
|
-
}
|
|
237
|
-
}]
|
|
238
|
-
};
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
3. **Start with PM2**
|
|
242
|
-
```bash
|
|
243
|
-
pm2 start ecosystem.config.js
|
|
244
|
-
pm2 save
|
|
245
|
-
pm2 startup
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### Systemd Service
|
|
249
|
-
|
|
250
|
-
Create `/etc/systemd/system/rondevu.service`:
|
|
251
|
-
|
|
252
|
-
```ini
|
|
253
|
-
[Unit]
|
|
254
|
-
Description=Rondevu Peer Discovery and Signaling Server
|
|
255
|
-
After=network.target
|
|
256
|
-
|
|
257
|
-
[Service]
|
|
258
|
-
Type=simple
|
|
259
|
-
User=www-data
|
|
260
|
-
WorkingDirectory=/opt/rondevu
|
|
261
|
-
ExecStart=/usr/bin/node dist/index.js
|
|
262
|
-
Restart=on-failure
|
|
263
|
-
Environment=PORT=3000
|
|
264
|
-
Environment=STORAGE_TYPE=sqlite
|
|
265
|
-
Environment=STORAGE_PATH=/opt/rondevu/data/sessions.db
|
|
266
|
-
Environment=SESSION_TIMEOUT=300000
|
|
267
|
-
Environment=CORS_ORIGINS=*
|
|
268
|
-
|
|
269
|
-
[Install]
|
|
270
|
-
WantedBy=multi-user.target
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
Enable and start:
|
|
274
|
-
```bash
|
|
275
|
-
sudo systemctl enable rondevu
|
|
276
|
-
sudo systemctl start rondevu
|
|
277
|
-
sudo systemctl status rondevu
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
---
|
|
281
|
-
|
|
282
|
-
## Troubleshooting
|
|
283
|
-
|
|
284
|
-
### Docker
|
|
285
|
-
|
|
286
|
-
**Issue: Permission denied on /app/data**
|
|
287
|
-
- Ensure volume permissions are correct
|
|
288
|
-
- The container runs as user `node` (UID 1000)
|
|
289
|
-
|
|
290
|
-
**Issue: Database locked**
|
|
291
|
-
- Don't share the same SQLite database file across multiple containers
|
|
292
|
-
- Use one instance or implement a different storage backend
|
|
293
|
-
|
|
294
|
-
### Node.js
|
|
295
|
-
|
|
296
|
-
**Issue: EADDRINUSE**
|
|
297
|
-
- Port is already in use, change `PORT` environment variable
|
|
298
|
-
|
|
299
|
-
**Issue: Database is locked**
|
|
300
|
-
- Another process is using the database
|
|
301
|
-
- Ensure only one instance is running with the same database file
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
|
-
## Performance Tuning
|
|
306
|
-
|
|
307
|
-
### Node.js/Docker
|
|
308
|
-
|
|
309
|
-
- Set `SESSION_TIMEOUT` appropriately to balance resource usage
|
|
310
|
-
- For high traffic, use `STORAGE_PATH=:memory:` with session replication
|
|
311
|
-
- Consider horizontal scaling with a shared database backend
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## Security Considerations
|
|
316
|
-
|
|
317
|
-
1. **HTTPS**: Always use HTTPS in production
|
|
318
|
-
- Use a reverse proxy (nginx, Caddy) for Node.js deployments
|
|
319
|
-
- Docker deployments should be behind a reverse proxy
|
|
320
|
-
|
|
321
|
-
2. **Rate Limiting**: Implement rate limiting at the proxy level
|
|
322
|
-
|
|
323
|
-
3. **CORS**: Configure CORS origins appropriately
|
|
324
|
-
- Don't use `*` in production
|
|
325
|
-
- Set specific allowed origins: `https://example.com,https://app.example.com`
|
|
326
|
-
|
|
327
|
-
4. **Input Validation**: SDP offers/answers are stored as-is; validate on client side
|
|
328
|
-
|
|
329
|
-
5. **Session Codes**: UUID v4 codes provide strong entropy (2^122 combinations)
|
|
330
|
-
|
|
331
|
-
6. **Origin Isolation**: Sessions are isolated by Origin header to organize topics by domain
|
|
332
|
-
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
## Scaling
|
|
336
|
-
|
|
337
|
-
### Horizontal Scaling
|
|
338
|
-
|
|
339
|
-
- **Docker/Node.js**: Use a shared database (not SQLite) for multiple instances
|
|
340
|
-
- Implement a Redis or PostgreSQL storage adapter
|
|
341
|
-
|
|
342
|
-
### Vertical Scaling
|
|
343
|
-
|
|
344
|
-
- Increase `SESSION_TIMEOUT` or cleanup frequency as needed
|
|
345
|
-
- Monitor database size and connection pool
|
|
346
|
-
- For Node.js, monitor memory usage and increase if needed
|