@xtr-dev/rondevu-server 0.5.0 → 0.5.6
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/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/.idea/modules.xml +8 -0
- package/.idea/rondevu-server.iml +8 -0
- package/.idea/workspace.xml +17 -0
- package/README.md +80 -199
- package/build.js +4 -1
- package/dist/index.js +2755 -1448
- package/dist/index.js.map +4 -4
- package/migrations/fresh_schema.sql +36 -41
- package/package.json +10 -4
- package/src/app.ts +38 -18
- package/src/config.ts +155 -9
- package/src/crypto.ts +362 -265
- package/src/index.ts +20 -25
- package/src/rpc.ts +658 -405
- package/src/storage/d1.ts +312 -263
- package/src/storage/factory.ts +69 -0
- package/src/storage/hash-id.ts +13 -9
- package/src/storage/memory.ts +559 -0
- package/src/storage/mysql.ts +588 -0
- package/src/storage/postgres.ts +595 -0
- package/src/storage/schemas/mysql.sql +59 -0
- package/src/storage/schemas/postgres.sql +64 -0
- package/src/storage/sqlite.ts +303 -269
- package/src/storage/types.ts +113 -113
- package/src/worker.ts +15 -34
- package/tests/integration/api.test.ts +395 -0
- package/tests/integration/setup.ts +170 -0
- package/wrangler.toml +25 -26
- package/ADVANCED.md +0 -502
|
@@ -1,51 +1,32 @@
|
|
|
1
|
-
-- Fresh schema for Rondevu
|
|
2
|
-
--
|
|
3
|
-
-- This is the complete schema without migration steps
|
|
1
|
+
-- Fresh schema for Rondevu (Tags-based)
|
|
2
|
+
-- Offers are standalone with tags for discovery
|
|
4
3
|
|
|
5
4
|
-- Drop existing tables if they exist
|
|
6
5
|
DROP TABLE IF EXISTS ice_candidates;
|
|
7
|
-
DROP TABLE IF EXISTS services;
|
|
8
6
|
DROP TABLE IF EXISTS offers;
|
|
9
|
-
DROP TABLE IF EXISTS
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
username TEXT PRIMARY KEY,
|
|
14
|
-
public_key TEXT NOT NULL UNIQUE,
|
|
15
|
-
claimed_at INTEGER NOT NULL,
|
|
16
|
-
expires_at INTEGER NOT NULL,
|
|
17
|
-
last_used INTEGER NOT NULL,
|
|
18
|
-
metadata TEXT,
|
|
19
|
-
CHECK(length(username) >= 3 AND length(username) <= 32)
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
CREATE INDEX idx_usernames_expires ON usernames(expires_at);
|
|
23
|
-
CREATE INDEX idx_usernames_public_key ON usernames(public_key);
|
|
7
|
+
DROP TABLE IF EXISTS services;
|
|
8
|
+
DROP TABLE IF EXISTS credentials;
|
|
9
|
+
DROP TABLE IF EXISTS rate_limits;
|
|
10
|
+
DROP TABLE IF EXISTS nonces;
|
|
24
11
|
|
|
25
|
-
--
|
|
26
|
-
CREATE TABLE
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
service_name TEXT NOT NULL,
|
|
30
|
-
version TEXT NOT NULL,
|
|
31
|
-
username TEXT NOT NULL,
|
|
12
|
+
-- Credentials table (name + secret auth)
|
|
13
|
+
CREATE TABLE credentials (
|
|
14
|
+
name TEXT PRIMARY KEY,
|
|
15
|
+
secret TEXT NOT NULL UNIQUE,
|
|
32
16
|
created_at INTEGER NOT NULL,
|
|
33
17
|
expires_at INTEGER NOT NULL,
|
|
34
|
-
|
|
35
|
-
|
|
18
|
+
last_used INTEGER NOT NULL,
|
|
19
|
+
CHECK(length(name) >= 3 AND length(name) <= 32)
|
|
36
20
|
);
|
|
37
21
|
|
|
38
|
-
CREATE INDEX
|
|
39
|
-
CREATE INDEX
|
|
40
|
-
CREATE INDEX idx_services_username ON services(username);
|
|
41
|
-
CREATE INDEX idx_services_expires ON services(expires_at);
|
|
22
|
+
CREATE INDEX idx_credentials_expires ON credentials(expires_at);
|
|
23
|
+
CREATE INDEX idx_credentials_secret ON credentials(secret);
|
|
42
24
|
|
|
43
|
-
-- Offers table (
|
|
25
|
+
-- Offers table (standalone with tags for discovery)
|
|
44
26
|
CREATE TABLE offers (
|
|
45
27
|
id TEXT PRIMARY KEY,
|
|
46
28
|
username TEXT NOT NULL,
|
|
47
|
-
|
|
48
|
-
service_fqn TEXT,
|
|
29
|
+
tags TEXT NOT NULL, -- JSON array: '["tag1", "tag2"]'
|
|
49
30
|
sdp TEXT NOT NULL,
|
|
50
31
|
created_at INTEGER NOT NULL,
|
|
51
32
|
expires_at INTEGER NOT NULL,
|
|
@@ -53,17 +34,15 @@ CREATE TABLE offers (
|
|
|
53
34
|
answerer_username TEXT,
|
|
54
35
|
answer_sdp TEXT,
|
|
55
36
|
answered_at INTEGER,
|
|
56
|
-
FOREIGN KEY (username) REFERENCES
|
|
57
|
-
FOREIGN KEY (answerer_username) REFERENCES usernames(username) ON DELETE SET NULL
|
|
37
|
+
FOREIGN KEY (username) REFERENCES credentials(name) ON DELETE CASCADE
|
|
58
38
|
);
|
|
59
39
|
|
|
60
40
|
CREATE INDEX idx_offers_username ON offers(username);
|
|
61
|
-
CREATE INDEX idx_offers_service ON offers(service_id);
|
|
62
41
|
CREATE INDEX idx_offers_expires ON offers(expires_at);
|
|
63
42
|
CREATE INDEX idx_offers_last_seen ON offers(last_seen);
|
|
64
43
|
CREATE INDEX idx_offers_answerer ON offers(answerer_username);
|
|
65
44
|
|
|
66
|
-
-- ICE candidates table
|
|
45
|
+
-- ICE candidates table
|
|
67
46
|
CREATE TABLE ice_candidates (
|
|
68
47
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
48
|
offer_id TEXT NOT NULL,
|
|
@@ -71,11 +50,27 @@ CREATE TABLE ice_candidates (
|
|
|
71
50
|
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
|
|
72
51
|
candidate TEXT NOT NULL,
|
|
73
52
|
created_at INTEGER NOT NULL,
|
|
74
|
-
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
75
|
-
FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE
|
|
53
|
+
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
76
54
|
);
|
|
77
55
|
|
|
78
56
|
CREATE INDEX idx_ice_offer ON ice_candidates(offer_id);
|
|
79
57
|
CREATE INDEX idx_ice_username ON ice_candidates(username);
|
|
80
58
|
CREATE INDEX idx_ice_role ON ice_candidates(role);
|
|
81
59
|
CREATE INDEX idx_ice_created ON ice_candidates(created_at);
|
|
60
|
+
|
|
61
|
+
-- Rate limits table
|
|
62
|
+
CREATE TABLE rate_limits (
|
|
63
|
+
identifier TEXT PRIMARY KEY,
|
|
64
|
+
count INTEGER NOT NULL,
|
|
65
|
+
reset_time INTEGER NOT NULL
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX idx_rate_limits_reset ON rate_limits(reset_time);
|
|
69
|
+
|
|
70
|
+
-- Nonces table (for replay attack prevention)
|
|
71
|
+
CREATE TABLE nonces (
|
|
72
|
+
nonce_key TEXT PRIMARY KEY,
|
|
73
|
+
expires_at INTEGER NOT NULL
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE INDEX idx_nonces_expires ON nonces(expires_at);
|
package/package.json
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtr-dev/rondevu-server",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "DNS-like WebRTC signaling server with
|
|
3
|
+
"version": "0.5.6",
|
|
4
|
+
"description": "DNS-like WebRTC signaling server with credential-based authentication and service discovery",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "node build.js",
|
|
8
8
|
"typecheck": "tsc",
|
|
9
9
|
"dev": "ts-node src/index.ts",
|
|
10
10
|
"start": "node dist/index.js",
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "node --import tsx --test tests/integration/*.test.ts",
|
|
12
|
+
"test:local": "API_URL=http://localhost:3000 node --import tsx --test tests/integration/*.test.ts",
|
|
12
13
|
"deploy": "npx wrangler deploy src/worker.ts --var VERSION:$(git rev-parse --short HEAD)"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
15
16
|
"@cloudflare/workers-types": "^4.20251014.0",
|
|
16
17
|
"@types/better-sqlite3": "^7.6.13",
|
|
17
18
|
"@types/node": "^24.9.2",
|
|
19
|
+
"@types/pg": "^8.15.4",
|
|
18
20
|
"esbuild": "^0.25.11",
|
|
19
21
|
"ts-node": "^10.9.2",
|
|
22
|
+
"tsx": "^4.19.0",
|
|
20
23
|
"typescript": "^5.9.3"
|
|
21
24
|
},
|
|
22
25
|
"dependencies": {
|
|
23
26
|
"@hono/node-server": "^1.19.6",
|
|
24
|
-
"@noble/ed25519": "^3.0.0",
|
|
25
27
|
"@xtr-dev/rondevu-client": "^0.13.0",
|
|
26
28
|
"better-sqlite3": "^12.4.1",
|
|
27
29
|
"hono": "^4.10.4"
|
|
30
|
+
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"mysql2": "^3.14.1",
|
|
33
|
+
"pg": "^8.16.0"
|
|
28
34
|
}
|
|
29
35
|
}
|
package/src/app.ts
CHANGED
|
@@ -4,9 +4,6 @@ import { Storage } from './storage/types.ts';
|
|
|
4
4
|
import { Config } from './config.ts';
|
|
5
5
|
import { handleRpc, RpcRequest } from './rpc.ts';
|
|
6
6
|
|
|
7
|
-
// Constants
|
|
8
|
-
const MAX_BATCH_SIZE = 100;
|
|
9
|
-
|
|
10
7
|
/**
|
|
11
8
|
* Creates the Hono application with RPC interface
|
|
12
9
|
*/
|
|
@@ -25,7 +22,7 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
25
22
|
return config.corsOrigins[0];
|
|
26
23
|
},
|
|
27
24
|
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
|
28
|
-
allowHeaders: ['Content-Type', 'Origin'],
|
|
25
|
+
allowHeaders: ['Content-Type', 'Origin', 'X-Name', 'X-Timestamp', 'X-Nonce', 'X-Signature'],
|
|
29
26
|
exposeHeaders: ['Content-Type'],
|
|
30
27
|
credentials: false,
|
|
31
28
|
maxAge: 86400,
|
|
@@ -36,7 +33,7 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
36
33
|
return c.json({
|
|
37
34
|
version: config.version,
|
|
38
35
|
name: 'Rondevu',
|
|
39
|
-
description: 'WebRTC signaling with RPC interface and
|
|
36
|
+
description: 'WebRTC signaling with RPC interface and HMAC signature-based authentication',
|
|
40
37
|
}, 200);
|
|
41
38
|
});
|
|
42
39
|
|
|
@@ -51,35 +48,58 @@ export function createApp(storage: Storage, config: Config) {
|
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
50
|
* POST /rpc
|
|
54
|
-
* RPC endpoint - accepts
|
|
51
|
+
* RPC endpoint - accepts batch method calls only
|
|
55
52
|
*/
|
|
56
53
|
app.post('/rpc', async (c) => {
|
|
57
54
|
try {
|
|
58
55
|
const body = await c.req.json();
|
|
59
56
|
|
|
60
|
-
//
|
|
61
|
-
|
|
57
|
+
// Only accept batch arrays
|
|
58
|
+
if (!Array.isArray(body)) {
|
|
59
|
+
return c.json([{
|
|
60
|
+
success: false,
|
|
61
|
+
error: 'Request must be an array of RPC calls',
|
|
62
|
+
errorCode: 'INVALID_PARAMS'
|
|
63
|
+
}], 400);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const requests: RpcRequest[] = body;
|
|
62
67
|
|
|
63
68
|
// Validate requests
|
|
64
69
|
if (requests.length === 0) {
|
|
65
|
-
return c.json({
|
|
70
|
+
return c.json([{
|
|
71
|
+
success: false,
|
|
72
|
+
error: 'Empty request array',
|
|
73
|
+
errorCode: 'INVALID_PARAMS'
|
|
74
|
+
}], 400);
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
if (requests.length >
|
|
69
|
-
return c.json({
|
|
77
|
+
if (requests.length > config.maxBatchSize) {
|
|
78
|
+
return c.json([{
|
|
79
|
+
success: false,
|
|
80
|
+
error: `Too many requests in batch (max ${config.maxBatchSize})`,
|
|
81
|
+
errorCode: 'BATCH_TOO_LARGE'
|
|
82
|
+
}], 413); // 413 Payload Too Large
|
|
70
83
|
}
|
|
71
84
|
|
|
72
|
-
// Handle RPC
|
|
73
|
-
const responses = await handleRpc(requests, storage, config);
|
|
85
|
+
// Handle RPC (pass context for auth headers)
|
|
86
|
+
const responses = await handleRpc(requests, c, storage, config);
|
|
74
87
|
|
|
75
|
-
//
|
|
76
|
-
return c.json(
|
|
88
|
+
// Always return array
|
|
89
|
+
return c.json(responses, 200);
|
|
77
90
|
} catch (err) {
|
|
78
91
|
console.error('RPC error:', err);
|
|
79
|
-
|
|
92
|
+
|
|
93
|
+
// Distinguish between JSON parse errors and validation errors
|
|
94
|
+
const errorMsg = err instanceof SyntaxError
|
|
95
|
+
? 'Invalid JSON in request body'
|
|
96
|
+
: 'Request must be valid JSON array';
|
|
97
|
+
|
|
98
|
+
return c.json([{
|
|
80
99
|
success: false,
|
|
81
|
-
error:
|
|
82
|
-
|
|
100
|
+
error: errorMsg,
|
|
101
|
+
errorCode: 'INVALID_PARAMS'
|
|
102
|
+
}], 400);
|
|
83
103
|
}
|
|
84
104
|
});
|
|
85
105
|
|
package/src/config.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { Storage } from './storage/types.ts';
|
|
2
|
+
import { StorageType } from './storage/factory.ts';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Application configuration
|
|
3
6
|
* Reads from environment variables with sensible defaults
|
|
4
7
|
*/
|
|
5
8
|
export interface Config {
|
|
6
9
|
port: number;
|
|
7
|
-
storageType:
|
|
10
|
+
storageType: StorageType;
|
|
8
11
|
storagePath: string;
|
|
12
|
+
databaseUrl: string;
|
|
13
|
+
dbPoolSize: number;
|
|
9
14
|
corsOrigins: string[];
|
|
10
15
|
version: string;
|
|
11
16
|
offerDefaultTtl: number;
|
|
@@ -13,24 +18,165 @@ export interface Config {
|
|
|
13
18
|
offerMinTtl: number;
|
|
14
19
|
cleanupInterval: number;
|
|
15
20
|
maxOffersPerRequest: number;
|
|
21
|
+
maxBatchSize: number;
|
|
22
|
+
maxSdpSize: number;
|
|
23
|
+
maxCandidateSize: number;
|
|
24
|
+
maxCandidateDepth: number;
|
|
25
|
+
maxCandidatesPerRequest: number;
|
|
26
|
+
maxTotalOperations: number;
|
|
27
|
+
timestampMaxAge: number; // Max age for timestamps (replay protection)
|
|
28
|
+
timestampMaxFuture: number; // Max future tolerance for timestamps (clock skew)
|
|
29
|
+
masterEncryptionKey: string; // 64-char hex string for encrypting secrets (32 bytes)
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
/**
|
|
19
33
|
* Loads configuration from environment variables
|
|
20
34
|
*/
|
|
21
35
|
export function loadConfig(): Config {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
// Master encryption key for secret storage
|
|
37
|
+
// CRITICAL: Set MASTER_ENCRYPTION_KEY in production to a secure random value
|
|
38
|
+
let masterEncryptionKey = process.env.MASTER_ENCRYPTION_KEY;
|
|
39
|
+
|
|
40
|
+
if (!masterEncryptionKey) {
|
|
41
|
+
// SECURITY: Fail fast unless explicitly in development mode
|
|
42
|
+
// Default to production-safe behavior if NODE_ENV is unset
|
|
43
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
44
|
+
|
|
45
|
+
if (!isDevelopment) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'MASTER_ENCRYPTION_KEY environment variable must be set. ' +
|
|
48
|
+
'Generate with: openssl rand -hex 32\n' +
|
|
49
|
+
'For development only, set NODE_ENV=development to use insecure dev key.'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Use deterministic key ONLY in explicit development mode
|
|
54
|
+
// WARNING: DO NOT USE THIS IN PRODUCTION - only for local development
|
|
55
|
+
console.error('⚠️ WARNING: Using insecure deterministic development key');
|
|
56
|
+
console.error('⚠️ ONLY use NODE_ENV=development for local development');
|
|
57
|
+
console.error('⚠️ Generate production key with: openssl rand -hex 32');
|
|
58
|
+
// Random-looking dev key (not ASCII-readable to prevent accidental production use)
|
|
59
|
+
masterEncryptionKey = 'a3f8b9c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate master encryption key format
|
|
63
|
+
// NOTE: Using regex here is safe since this runs at startup, not during request processing
|
|
64
|
+
if (masterEncryptionKey.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(masterEncryptionKey)) {
|
|
65
|
+
throw new Error('MASTER_ENCRYPTION_KEY must be 64-character hex string (32 bytes). Generate with: openssl rand -hex 32');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper to safely parse and validate integer config values
|
|
69
|
+
function parsePositiveInt(value: string | undefined, defaultValue: string, name: string, min = 1): number {
|
|
70
|
+
const parsed = parseInt(value || defaultValue, 10);
|
|
71
|
+
if (isNaN(parsed)) {
|
|
72
|
+
throw new Error(`${name} must be a valid integer (got: ${value})`);
|
|
73
|
+
}
|
|
74
|
+
if (parsed < min) {
|
|
75
|
+
throw new Error(`${name} must be >= ${min} (got: ${parsed})`);
|
|
76
|
+
}
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const config = {
|
|
81
|
+
port: parsePositiveInt(process.env.PORT, '3000', 'PORT', 1),
|
|
82
|
+
storageType: (process.env.STORAGE_TYPE || 'memory') as StorageType,
|
|
25
83
|
storagePath: process.env.STORAGE_PATH || ':memory:',
|
|
84
|
+
databaseUrl: process.env.DATABASE_URL || '',
|
|
85
|
+
dbPoolSize: parsePositiveInt(process.env.DB_POOL_SIZE, '10', 'DB_POOL_SIZE', 1),
|
|
26
86
|
corsOrigins: process.env.CORS_ORIGINS
|
|
27
87
|
? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
|
|
28
88
|
: ['*'],
|
|
29
89
|
version: process.env.VERSION || 'unknown',
|
|
30
|
-
offerDefaultTtl:
|
|
31
|
-
offerMaxTtl:
|
|
32
|
-
offerMinTtl:
|
|
33
|
-
cleanupInterval:
|
|
34
|
-
maxOffersPerRequest:
|
|
90
|
+
offerDefaultTtl: parsePositiveInt(process.env.OFFER_DEFAULT_TTL, '60000', 'OFFER_DEFAULT_TTL', 1000),
|
|
91
|
+
offerMaxTtl: parsePositiveInt(process.env.OFFER_MAX_TTL, '86400000', 'OFFER_MAX_TTL', 1000),
|
|
92
|
+
offerMinTtl: parsePositiveInt(process.env.OFFER_MIN_TTL, '60000', 'OFFER_MIN_TTL', 1000),
|
|
93
|
+
cleanupInterval: parsePositiveInt(process.env.CLEANUP_INTERVAL, '60000', 'CLEANUP_INTERVAL', 1000),
|
|
94
|
+
maxOffersPerRequest: parsePositiveInt(process.env.MAX_OFFERS_PER_REQUEST, '100', 'MAX_OFFERS_PER_REQUEST', 1),
|
|
95
|
+
maxBatchSize: parsePositiveInt(process.env.MAX_BATCH_SIZE, '100', 'MAX_BATCH_SIZE', 1),
|
|
96
|
+
maxSdpSize: parsePositiveInt(process.env.MAX_SDP_SIZE, String(64 * 1024), 'MAX_SDP_SIZE', 1024), // Min 1KB
|
|
97
|
+
maxCandidateSize: parsePositiveInt(process.env.MAX_CANDIDATE_SIZE, String(4 * 1024), 'MAX_CANDIDATE_SIZE', 256), // Min 256 bytes
|
|
98
|
+
maxCandidateDepth: parsePositiveInt(process.env.MAX_CANDIDATE_DEPTH, '10', 'MAX_CANDIDATE_DEPTH', 1),
|
|
99
|
+
maxCandidatesPerRequest: parsePositiveInt(process.env.MAX_CANDIDATES_PER_REQUEST, '100', 'MAX_CANDIDATES_PER_REQUEST', 1),
|
|
100
|
+
maxTotalOperations: parsePositiveInt(process.env.MAX_TOTAL_OPERATIONS, '1000', 'MAX_TOTAL_OPERATIONS', 1),
|
|
101
|
+
timestampMaxAge: parsePositiveInt(process.env.TIMESTAMP_MAX_AGE, '60000', 'TIMESTAMP_MAX_AGE', 1000), // Min 1 second
|
|
102
|
+
timestampMaxFuture: parsePositiveInt(process.env.TIMESTAMP_MAX_FUTURE, '60000', 'TIMESTAMP_MAX_FUTURE', 1000), // Min 1 second
|
|
103
|
+
masterEncryptionKey,
|
|
35
104
|
};
|
|
105
|
+
|
|
106
|
+
return config;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Default config values (shared between Node and Workers)
|
|
111
|
+
*/
|
|
112
|
+
export const CONFIG_DEFAULTS = {
|
|
113
|
+
offerDefaultTtl: 60000,
|
|
114
|
+
offerMaxTtl: 86400000,
|
|
115
|
+
offerMinTtl: 60000,
|
|
116
|
+
cleanupInterval: 60000,
|
|
117
|
+
maxOffersPerRequest: 100,
|
|
118
|
+
maxBatchSize: 100,
|
|
119
|
+
maxSdpSize: 64 * 1024,
|
|
120
|
+
maxCandidateSize: 4 * 1024,
|
|
121
|
+
maxCandidateDepth: 10,
|
|
122
|
+
maxCandidatesPerRequest: 100,
|
|
123
|
+
maxTotalOperations: 1000,
|
|
124
|
+
timestampMaxAge: 60000,
|
|
125
|
+
timestampMaxFuture: 60000,
|
|
126
|
+
} as const;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build config for Cloudflare Workers from env vars
|
|
130
|
+
*/
|
|
131
|
+
export function buildWorkerConfig(env: {
|
|
132
|
+
MASTER_ENCRYPTION_KEY: string;
|
|
133
|
+
OFFER_DEFAULT_TTL?: string;
|
|
134
|
+
OFFER_MAX_TTL?: string;
|
|
135
|
+
OFFER_MIN_TTL?: string;
|
|
136
|
+
MAX_OFFERS_PER_REQUEST?: string;
|
|
137
|
+
MAX_BATCH_SIZE?: string;
|
|
138
|
+
CORS_ORIGINS?: string;
|
|
139
|
+
VERSION?: string;
|
|
140
|
+
}): Config {
|
|
141
|
+
return {
|
|
142
|
+
port: 0, // Not used in Workers
|
|
143
|
+
storageType: 'sqlite', // D1 is SQLite-compatible
|
|
144
|
+
storagePath: '', // Not used with D1
|
|
145
|
+
databaseUrl: '', // Not used with D1
|
|
146
|
+
dbPoolSize: 10, // Not used with D1
|
|
147
|
+
corsOrigins: env.CORS_ORIGINS?.split(',').map(o => o.trim()) ?? ['*'],
|
|
148
|
+
version: env.VERSION ?? 'unknown',
|
|
149
|
+
offerDefaultTtl: env.OFFER_DEFAULT_TTL ? parseInt(env.OFFER_DEFAULT_TTL, 10) : CONFIG_DEFAULTS.offerDefaultTtl,
|
|
150
|
+
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : CONFIG_DEFAULTS.offerMaxTtl,
|
|
151
|
+
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : CONFIG_DEFAULTS.offerMinTtl,
|
|
152
|
+
cleanupInterval: CONFIG_DEFAULTS.cleanupInterval,
|
|
153
|
+
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : CONFIG_DEFAULTS.maxOffersPerRequest,
|
|
154
|
+
maxBatchSize: env.MAX_BATCH_SIZE ? parseInt(env.MAX_BATCH_SIZE, 10) : CONFIG_DEFAULTS.maxBatchSize,
|
|
155
|
+
maxSdpSize: CONFIG_DEFAULTS.maxSdpSize,
|
|
156
|
+
maxCandidateSize: CONFIG_DEFAULTS.maxCandidateSize,
|
|
157
|
+
maxCandidateDepth: CONFIG_DEFAULTS.maxCandidateDepth,
|
|
158
|
+
maxCandidatesPerRequest: CONFIG_DEFAULTS.maxCandidatesPerRequest,
|
|
159
|
+
maxTotalOperations: CONFIG_DEFAULTS.maxTotalOperations,
|
|
160
|
+
timestampMaxAge: CONFIG_DEFAULTS.timestampMaxAge,
|
|
161
|
+
timestampMaxFuture: CONFIG_DEFAULTS.timestampMaxFuture,
|
|
162
|
+
masterEncryptionKey: env.MASTER_ENCRYPTION_KEY,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run cleanup of expired entries (shared between Node and Workers)
|
|
168
|
+
* @returns Object with counts of deleted items
|
|
169
|
+
*/
|
|
170
|
+
export async function runCleanup(storage: Storage, now: number): Promise<{
|
|
171
|
+
offers: number;
|
|
172
|
+
credentials: number;
|
|
173
|
+
rateLimits: number;
|
|
174
|
+
nonces: number;
|
|
175
|
+
}> {
|
|
176
|
+
const offers = await storage.deleteExpiredOffers(now);
|
|
177
|
+
const credentials = await storage.deleteExpiredCredentials(now);
|
|
178
|
+
const rateLimits = await storage.deleteExpiredRateLimits(now);
|
|
179
|
+
const nonces = await storage.deleteExpiredNonces(now);
|
|
180
|
+
|
|
181
|
+
return { offers, credentials, rateLimits, nonces };
|
|
36
182
|
}
|