@xtr-dev/rondevu-server 0.5.11 → 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.
- package/README.md +9 -21
- package/dist/index.js +39 -19
- package/dist/index.js.map +2 -2
- package/migrations/0009_public_key_auth.sql +74 -0
- package/migrations/fresh_schema.sql +20 -21
- package/package.json +2 -1
- package/src/config.ts +1 -47
- package/src/crypto.ts +70 -304
- package/src/index.ts +2 -3
- package/src/rpc.ts +95 -271
- package/src/storage/d1.ts +77 -236
- package/src/storage/factory.ts +4 -17
- package/src/storage/memory.ts +49 -152
- package/src/storage/mysql.ts +71 -188
- package/src/storage/postgres.ts +72 -188
- package/src/storage/sqlite.ts +70 -195
- package/src/storage/types.ts +32 -88
- package/src/worker.ts +4 -9
|
@@ -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 (
|
|
2
|
-
--
|
|
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
|
|
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
|
-
--
|
|
13
|
-
CREATE TABLE
|
|
14
|
-
|
|
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(
|
|
18
|
+
CHECK(length(public_key) = 64)
|
|
20
19
|
);
|
|
21
20
|
|
|
22
|
-
CREATE INDEX
|
|
23
|
-
CREATE INDEX idx_credentials_secret ON credentials(secret);
|
|
21
|
+
CREATE INDEX idx_identities_expires ON identities(expires_at);
|
|
24
22
|
|
|
25
|
-
-- Offers table (
|
|
23
|
+
-- Offers table (uses public_key instead of username)
|
|
26
24
|
CREATE TABLE offers (
|
|
27
25
|
id TEXT PRIMARY KEY,
|
|
28
|
-
|
|
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
|
-
|
|
32
|
+
answerer_public_key TEXT,
|
|
35
33
|
answer_sdp TEXT,
|
|
36
34
|
answered_at INTEGER,
|
|
37
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
167
|
+
return { offers, rateLimits, nonces };
|
|
214
168
|
}
|