@xtr-dev/rondevu-server 0.5.12 → 0.5.13

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.
@@ -0,0 +1,74 @@
1
+ -- Migration: Replace HMAC credentials with Ed25519 public key identities
2
+ -- This is a breaking change - all existing data will be lost
3
+
4
+ -- Drop existing tables (clean slate)
5
+ DROP TABLE IF EXISTS ice_candidates;
6
+ DROP TABLE IF EXISTS offers;
7
+ DROP TABLE IF EXISTS credentials;
8
+ DROP TABLE IF EXISTS rate_limits;
9
+ DROP TABLE IF EXISTS nonces;
10
+
11
+ -- Identities table (Ed25519 public key as identity)
12
+ CREATE TABLE identities (
13
+ public_key TEXT PRIMARY KEY, -- 64-char hex Ed25519 public key
14
+ created_at INTEGER NOT NULL,
15
+ expires_at INTEGER NOT NULL, -- 1 year from creation/last use
16
+ last_used INTEGER NOT NULL,
17
+ CHECK(length(public_key) = 64)
18
+ );
19
+
20
+ CREATE INDEX idx_identities_expires ON identities(expires_at);
21
+
22
+ -- Offers table (uses public_key instead of username)
23
+ CREATE TABLE offers (
24
+ id TEXT PRIMARY KEY,
25
+ public_key TEXT NOT NULL, -- Owner's Ed25519 public key
26
+ tags TEXT NOT NULL, -- JSON array: '["tag1", "tag2"]'
27
+ sdp TEXT NOT NULL,
28
+ created_at INTEGER NOT NULL,
29
+ expires_at INTEGER NOT NULL,
30
+ last_seen INTEGER NOT NULL,
31
+ answerer_public_key TEXT,
32
+ answer_sdp TEXT,
33
+ answered_at INTEGER,
34
+ matched_tags TEXT, -- JSON array: tags the answerer searched for
35
+ FOREIGN KEY (public_key) REFERENCES identities(public_key) ON DELETE CASCADE
36
+ );
37
+
38
+ CREATE INDEX idx_offers_public_key ON offers(public_key);
39
+ CREATE INDEX idx_offers_expires ON offers(expires_at);
40
+ CREATE INDEX idx_offers_last_seen ON offers(last_seen);
41
+ CREATE INDEX idx_offers_answerer ON offers(answerer_public_key);
42
+
43
+ -- ICE candidates table (uses public_key instead of username)
44
+ CREATE TABLE ice_candidates (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ offer_id TEXT NOT NULL,
47
+ public_key TEXT NOT NULL, -- Sender's Ed25519 public key
48
+ role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
49
+ candidate TEXT NOT NULL,
50
+ created_at INTEGER NOT NULL,
51
+ FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
52
+ );
53
+
54
+ CREATE INDEX idx_ice_offer ON ice_candidates(offer_id);
55
+ CREATE INDEX idx_ice_public_key ON ice_candidates(public_key);
56
+ CREATE INDEX idx_ice_role ON ice_candidates(role);
57
+ CREATE INDEX idx_ice_created ON ice_candidates(created_at);
58
+
59
+ -- Rate limits table (unchanged)
60
+ CREATE TABLE rate_limits (
61
+ identifier TEXT PRIMARY KEY,
62
+ count INTEGER NOT NULL,
63
+ reset_time INTEGER NOT NULL
64
+ );
65
+
66
+ CREATE INDEX idx_rate_limits_reset ON rate_limits(reset_time);
67
+
68
+ -- Nonces table (unchanged)
69
+ CREATE TABLE nonces (
70
+ nonce_key TEXT PRIMARY KEY,
71
+ expires_at INTEGER NOT NULL
72
+ );
73
+
74
+ CREATE INDEX idx_nonces_expires ON nonces(expires_at);
@@ -1,52 +1,51 @@
1
- -- Fresh schema for Rondevu (Tags-based)
2
- -- Offers are standalone with tags for discovery
1
+ -- Fresh schema for Rondevu (Ed25519 Public Key Identity)
2
+ -- The public key IS the identity - no usernames
3
3
 
4
4
  -- Drop existing tables if they exist
5
5
  DROP TABLE IF EXISTS ice_candidates;
6
6
  DROP TABLE IF EXISTS offers;
7
- DROP TABLE IF EXISTS services;
8
- DROP TABLE IF EXISTS credentials;
7
+ DROP TABLE IF EXISTS identities;
8
+ DROP TABLE IF EXISTS credentials; -- Legacy, remove if exists
9
9
  DROP TABLE IF EXISTS rate_limits;
10
10
  DROP TABLE IF EXISTS nonces;
11
11
 
12
- -- Credentials table (name + secret auth)
13
- CREATE TABLE credentials (
14
- name TEXT PRIMARY KEY,
15
- secret TEXT NOT NULL UNIQUE,
12
+ -- Identities table (Ed25519 public key as identity)
13
+ CREATE TABLE identities (
14
+ public_key TEXT PRIMARY KEY, -- 64-char hex Ed25519 public key
16
15
  created_at INTEGER NOT NULL,
17
- expires_at INTEGER NOT NULL,
16
+ expires_at INTEGER NOT NULL, -- 1 year from creation/last use
18
17
  last_used INTEGER NOT NULL,
19
- CHECK(length(name) >= 3 AND length(name) <= 32)
18
+ CHECK(length(public_key) = 64)
20
19
  );
21
20
 
22
- CREATE INDEX idx_credentials_expires ON credentials(expires_at);
23
- CREATE INDEX idx_credentials_secret ON credentials(secret);
21
+ CREATE INDEX idx_identities_expires ON identities(expires_at);
24
22
 
25
- -- Offers table (standalone with tags for discovery)
23
+ -- Offers table (uses public_key instead of username)
26
24
  CREATE TABLE offers (
27
25
  id TEXT PRIMARY KEY,
28
- username TEXT NOT NULL,
26
+ public_key TEXT NOT NULL, -- Owner's Ed25519 public key
29
27
  tags TEXT NOT NULL, -- JSON array: '["tag1", "tag2"]'
30
28
  sdp TEXT NOT NULL,
31
29
  created_at INTEGER NOT NULL,
32
30
  expires_at INTEGER NOT NULL,
33
31
  last_seen INTEGER NOT NULL,
34
- answerer_username TEXT,
32
+ answerer_public_key TEXT,
35
33
  answer_sdp TEXT,
36
34
  answered_at INTEGER,
37
- FOREIGN KEY (username) REFERENCES credentials(name) ON DELETE CASCADE
35
+ matched_tags TEXT, -- JSON array: tags the answerer searched for
36
+ FOREIGN KEY (public_key) REFERENCES identities(public_key) ON DELETE CASCADE
38
37
  );
39
38
 
40
- CREATE INDEX idx_offers_username ON offers(username);
39
+ CREATE INDEX idx_offers_public_key ON offers(public_key);
41
40
  CREATE INDEX idx_offers_expires ON offers(expires_at);
42
41
  CREATE INDEX idx_offers_last_seen ON offers(last_seen);
43
- CREATE INDEX idx_offers_answerer ON offers(answerer_username);
42
+ CREATE INDEX idx_offers_answerer ON offers(answerer_public_key);
44
43
 
45
- -- ICE candidates table
44
+ -- ICE candidates table (uses public_key instead of username)
46
45
  CREATE TABLE ice_candidates (
47
46
  id INTEGER PRIMARY KEY AUTOINCREMENT,
48
47
  offer_id TEXT NOT NULL,
49
- username TEXT NOT NULL,
48
+ public_key TEXT NOT NULL, -- Sender's Ed25519 public key
50
49
  role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
51
50
  candidate TEXT NOT NULL,
52
51
  created_at INTEGER NOT NULL,
@@ -54,7 +53,7 @@ CREATE TABLE ice_candidates (
54
53
  );
55
54
 
56
55
  CREATE INDEX idx_ice_offer ON ice_candidates(offer_id);
57
- CREATE INDEX idx_ice_username ON ice_candidates(username);
56
+ CREATE INDEX idx_ice_public_key ON ice_candidates(public_key);
58
57
  CREATE INDEX idx_ice_role ON ice_candidates(role);
59
58
  CREATE INDEX idx_ice_created ON ice_candidates(created_at);
60
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-server",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "DNS-like WebRTC signaling server with credential-based authentication and service discovery",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@hono/node-server": "^1.19.6",
27
+ "@noble/ed25519": "^3.0.0",
27
28
  "@xtr-dev/rondevu-client": "^0.13.0",
28
29
  "better-sqlite3": "^12.4.1",
29
30
  "hono": "^4.10.4"
package/src/config.ts CHANGED
@@ -30,13 +30,10 @@ export interface Config {
30
30
  maxTotalOperations: number;
31
31
  timestampMaxAge: number; // Max age for timestamps (replay protection)
32
32
  timestampMaxFuture: number; // Max future tolerance for timestamps (clock skew)
33
- masterEncryptionKey: string; // 64-char hex string for encrypting secrets (32 bytes)
34
33
  // Resource limits (for abuse prevention)
35
34
  maxOffersPerUser: number; // Max concurrent offers per user
36
35
  maxTotalOffers: number; // Max total offers in storage
37
- maxTotalCredentials: number; // Max total credentials in storage
38
36
  maxIceCandidatesPerOffer: number; // Max ICE candidates per offer
39
- credentialsPerIpPerSecond: number; // Rate limit: credentials per IP per second
40
37
  requestsPerIpPerSecond: number; // Rate limit: requests per IP per second
41
38
  }
42
39
 
@@ -44,38 +41,6 @@ export interface Config {
44
41
  * Loads configuration from environment variables
45
42
  */
46
43
  export function loadConfig(): Config {
47
- // Master encryption key for secret storage
48
- // CRITICAL: Set MASTER_ENCRYPTION_KEY in production to a secure random value
49
- let masterEncryptionKey = process.env.MASTER_ENCRYPTION_KEY;
50
-
51
- if (!masterEncryptionKey) {
52
- // SECURITY: Fail fast unless explicitly in development mode
53
- // Default to production-safe behavior if NODE_ENV is unset
54
- const isDevelopment = process.env.NODE_ENV === 'development';
55
-
56
- if (!isDevelopment) {
57
- throw new Error(
58
- 'MASTER_ENCRYPTION_KEY environment variable must be set. ' +
59
- 'Generate with: openssl rand -hex 32\n' +
60
- 'For development only, set NODE_ENV=development to use insecure dev key.'
61
- );
62
- }
63
-
64
- // Use deterministic key ONLY in explicit development mode
65
- // WARNING: DO NOT USE THIS IN PRODUCTION - only for local development
66
- console.error('⚠️ WARNING: Using insecure deterministic development key');
67
- console.error('⚠️ ONLY use NODE_ENV=development for local development');
68
- console.error('⚠️ Generate production key with: openssl rand -hex 32');
69
- // Random-looking dev key (not ASCII-readable to prevent accidental production use)
70
- masterEncryptionKey = 'a3f8b9c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0';
71
- }
72
-
73
- // Validate master encryption key format
74
- // NOTE: Using regex here is safe since this runs at startup, not during request processing
75
- if (masterEncryptionKey.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(masterEncryptionKey)) {
76
- throw new Error('MASTER_ENCRYPTION_KEY must be 64-character hex string (32 bytes). Generate with: openssl rand -hex 32');
77
- }
78
-
79
44
  // Helper to safely parse and validate integer config values
80
45
  function parsePositiveInt(value: string | undefined, defaultValue: string, name: string, min = 1): number {
81
46
  const parsed = parseInt(value || defaultValue, 10);
@@ -111,13 +76,10 @@ export function loadConfig(): Config {
111
76
  maxTotalOperations: parsePositiveInt(process.env.MAX_TOTAL_OPERATIONS, '1000', 'MAX_TOTAL_OPERATIONS', 1),
112
77
  timestampMaxAge: parsePositiveInt(process.env.TIMESTAMP_MAX_AGE, '60000', 'TIMESTAMP_MAX_AGE', 1000), // Min 1 second
113
78
  timestampMaxFuture: parsePositiveInt(process.env.TIMESTAMP_MAX_FUTURE, '60000', 'TIMESTAMP_MAX_FUTURE', 1000), // Min 1 second
114
- masterEncryptionKey,
115
79
  // Resource limits
116
80
  maxOffersPerUser: parsePositiveInt(process.env.MAX_OFFERS_PER_USER, '1000', 'MAX_OFFERS_PER_USER', 1),
117
81
  maxTotalOffers: parsePositiveInt(process.env.MAX_TOTAL_OFFERS, '100000', 'MAX_TOTAL_OFFERS', 1),
118
- maxTotalCredentials: parsePositiveInt(process.env.MAX_TOTAL_CREDENTIALS, '50000', 'MAX_TOTAL_CREDENTIALS', 1),
119
82
  maxIceCandidatesPerOffer: parsePositiveInt(process.env.MAX_ICE_CANDIDATES_PER_OFFER, '50', 'MAX_ICE_CANDIDATES_PER_OFFER', 1),
120
- credentialsPerIpPerSecond: parsePositiveInt(process.env.CREDENTIALS_PER_IP_PER_SECOND, '5', 'CREDENTIALS_PER_IP_PER_SECOND', 1),
121
83
  requestsPerIpPerSecond: parsePositiveInt(process.env.REQUESTS_PER_IP_PER_SECOND, '50', 'REQUESTS_PER_IP_PER_SECOND', 1),
122
84
  };
123
85
 
@@ -144,9 +106,7 @@ export const CONFIG_DEFAULTS = {
144
106
  // Resource limits
145
107
  maxOffersPerUser: 1000,
146
108
  maxTotalOffers: 100000,
147
- maxTotalCredentials: 50000,
148
109
  maxIceCandidatesPerOffer: 50,
149
- credentialsPerIpPerSecond: 5,
150
110
  requestsPerIpPerSecond: 50,
151
111
  } as const;
152
112
 
@@ -154,7 +114,6 @@ export const CONFIG_DEFAULTS = {
154
114
  * Build config for Cloudflare Workers from env vars
155
115
  */
156
116
  export function buildWorkerConfig(env: {
157
- MASTER_ENCRYPTION_KEY: string;
158
117
  OFFER_DEFAULT_TTL?: string;
159
118
  OFFER_MAX_TTL?: string;
160
119
  OFFER_MIN_TTL?: string;
@@ -184,13 +143,10 @@ export function buildWorkerConfig(env: {
184
143
  maxTotalOperations: CONFIG_DEFAULTS.maxTotalOperations,
185
144
  timestampMaxAge: CONFIG_DEFAULTS.timestampMaxAge,
186
145
  timestampMaxFuture: CONFIG_DEFAULTS.timestampMaxFuture,
187
- masterEncryptionKey: env.MASTER_ENCRYPTION_KEY,
188
146
  // Resource limits
189
147
  maxOffersPerUser: CONFIG_DEFAULTS.maxOffersPerUser,
190
148
  maxTotalOffers: CONFIG_DEFAULTS.maxTotalOffers,
191
- maxTotalCredentials: CONFIG_DEFAULTS.maxTotalCredentials,
192
149
  maxIceCandidatesPerOffer: CONFIG_DEFAULTS.maxIceCandidatesPerOffer,
193
- credentialsPerIpPerSecond: CONFIG_DEFAULTS.credentialsPerIpPerSecond,
194
150
  requestsPerIpPerSecond: CONFIG_DEFAULTS.requestsPerIpPerSecond,
195
151
  };
196
152
  }
@@ -201,14 +157,12 @@ export function buildWorkerConfig(env: {
201
157
  */
202
158
  export async function runCleanup(storage: Storage, now: number): Promise<{
203
159
  offers: number;
204
- credentials: number;
205
160
  rateLimits: number;
206
161
  nonces: number;
207
162
  }> {
208
163
  const offers = await storage.deleteExpiredOffers(now);
209
- const credentials = await storage.deleteExpiredCredentials(now);
210
164
  const rateLimits = await storage.deleteExpiredRateLimits(now);
211
165
  const nonces = await storage.deleteExpiredNonces(now);
212
166
 
213
- return { offers, credentials, rateLimits, nonces };
167
+ return { offers, rateLimits, nonces };
214
168
  }