@xtr-dev/rondevu-server 0.5.1 → 0.5.7
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 +2891 -1446
- 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 +183 -9
- package/src/crypto.ts +361 -263
- package/src/index.ts +20 -25
- package/src/rpc.ts +714 -403
- package/src/storage/d1.ts +338 -263
- package/src/storage/factory.ts +69 -0
- package/src/storage/hash-id.ts +13 -9
- package/src/storage/memory.ts +579 -0
- package/src/storage/mysql.ts +616 -0
- package/src/storage/postgres.ts +623 -0
- package/src/storage/schemas/mysql.sql +59 -0
- package/src/storage/schemas/postgres.sql +64 -0
- package/src/storage/sqlite.ts +325 -269
- package/src/storage/types.ts +137 -109
- 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
package/src/storage/types.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Represents a WebRTC signaling offer
|
|
2
|
+
* Represents a WebRTC signaling offer with tags for discovery
|
|
3
3
|
*/
|
|
4
4
|
export interface Offer {
|
|
5
5
|
id: string;
|
|
6
6
|
username: string;
|
|
7
|
-
|
|
8
|
-
serviceFqn?: string; // Denormalized service FQN for easier queries
|
|
7
|
+
tags: string[]; // Tags for discovery (match ANY)
|
|
9
8
|
sdp: string;
|
|
10
9
|
createdAt: number;
|
|
11
10
|
expiresAt: number;
|
|
@@ -34,60 +33,46 @@ export interface IceCandidate {
|
|
|
34
33
|
export interface CreateOfferRequest {
|
|
35
34
|
id?: string;
|
|
36
35
|
username: string;
|
|
37
|
-
|
|
38
|
-
serviceFqn?: string; // Optional service FQN
|
|
36
|
+
tags: string[]; // Tags for discovery
|
|
39
37
|
sdp: string;
|
|
40
38
|
expiresAt: number;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
|
-
* Represents a
|
|
42
|
+
* Represents a credential (random name + secret pair)
|
|
43
|
+
* Replaces the old username/publicKey system for simpler authentication
|
|
45
44
|
*/
|
|
46
|
-
export interface
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
claimedAt: number;
|
|
50
|
-
expiresAt: number; // 365 days from claim/last use
|
|
51
|
-
lastUsed: number;
|
|
52
|
-
metadata?: string; // JSON optional user metadata
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Request to claim a username
|
|
57
|
-
*/
|
|
58
|
-
export interface ClaimUsernameRequest {
|
|
59
|
-
username: string;
|
|
60
|
-
publicKey: string;
|
|
61
|
-
signature: string;
|
|
62
|
-
message: string; // "claim:{username}:{timestamp}"
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Represents a published service (can have multiple offers)
|
|
67
|
-
* New format: service:version@username (e.g., chat:1.0.0@alice)
|
|
68
|
-
*/
|
|
69
|
-
export interface Service {
|
|
70
|
-
id: string; // UUID v4
|
|
71
|
-
serviceFqn: string; // Full FQN: chat:1.0.0@alice
|
|
72
|
-
serviceName: string; // Extracted: chat
|
|
73
|
-
version: string; // Extracted: 1.0.0
|
|
74
|
-
username: string; // Extracted: alice
|
|
45
|
+
export interface Credential {
|
|
46
|
+
name: string; // Random name (e.g., "brave-tiger-7a3f")
|
|
47
|
+
secret: string; // Random secret (API key style)
|
|
75
48
|
createdAt: number;
|
|
76
|
-
expiresAt: number;
|
|
49
|
+
expiresAt: number; // 365 days from creation/last use
|
|
50
|
+
lastUsed: number;
|
|
77
51
|
}
|
|
78
52
|
|
|
79
53
|
/**
|
|
80
|
-
* Request to
|
|
54
|
+
* Request to generate new credentials
|
|
81
55
|
*/
|
|
82
|
-
export interface
|
|
83
|
-
|
|
84
|
-
expiresAt
|
|
85
|
-
offers: CreateOfferRequest[]; // Multiple offers per service
|
|
56
|
+
export interface GenerateCredentialsRequest {
|
|
57
|
+
name?: string; // Optional: claim specific username (must be unique, 4-32 chars)
|
|
58
|
+
expiresAt?: number; // Optional: override default expiry
|
|
86
59
|
}
|
|
87
60
|
|
|
88
61
|
/**
|
|
89
|
-
* Storage interface for rondevu
|
|
62
|
+
* Storage interface for rondevu signaling system
|
|
90
63
|
* Implementations can use different backends (SQLite, D1, etc.)
|
|
64
|
+
*
|
|
65
|
+
* TRUST BOUNDARY: The storage layer assumes inputs are pre-validated by the RPC layer.
|
|
66
|
+
* This avoids duplication of validation logic across storage backends.
|
|
67
|
+
* The RPC layer is responsible for:
|
|
68
|
+
* - Validating tags format
|
|
69
|
+
* - Validating role is 'offerer' or 'answerer'
|
|
70
|
+
* - Validating all string parameters are non-empty
|
|
71
|
+
* - Validating timestamps and expirations
|
|
72
|
+
* - Verifying authentication and authorization
|
|
73
|
+
*
|
|
74
|
+
* Storage implementations may add defensive checks for critical invariants,
|
|
75
|
+
* but should not duplicate all RPC-layer validation.
|
|
91
76
|
*/
|
|
92
77
|
export interface Storage {
|
|
93
78
|
// ===== Offer Management =====
|
|
@@ -147,6 +132,42 @@ export interface Storage {
|
|
|
147
132
|
*/
|
|
148
133
|
getAnsweredOffers(offererUsername: string): Promise<Offer[]>;
|
|
149
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Retrieves all offers answered by a specific user (where they are the answerer)
|
|
137
|
+
* @param answererUsername Answerer's username
|
|
138
|
+
* @returns Array of offers the user has answered
|
|
139
|
+
*/
|
|
140
|
+
getOffersAnsweredBy(answererUsername: string): Promise<Offer[]>;
|
|
141
|
+
|
|
142
|
+
// ===== Discovery =====
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Discovers offers by tags with pagination
|
|
146
|
+
* Returns available offers (where answerer_username IS NULL) matching ANY of the provided tags
|
|
147
|
+
* @param tags Array of tags to match (OR logic)
|
|
148
|
+
* @param excludeUsername Optional username to exclude from results (self-exclusion)
|
|
149
|
+
* @param limit Maximum number of offers to return
|
|
150
|
+
* @param offset Number of offers to skip
|
|
151
|
+
* @returns Array of available offers matching tags
|
|
152
|
+
*/
|
|
153
|
+
discoverOffers(
|
|
154
|
+
tags: string[],
|
|
155
|
+
excludeUsername: string | null,
|
|
156
|
+
limit: number,
|
|
157
|
+
offset: number
|
|
158
|
+
): Promise<Offer[]>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Gets a random available offer matching any of the provided tags
|
|
162
|
+
* @param tags Array of tags to match (OR logic)
|
|
163
|
+
* @param excludeUsername Optional username to exclude (self-exclusion)
|
|
164
|
+
* @returns Random available offer, or null if none found
|
|
165
|
+
*/
|
|
166
|
+
getRandomOffer(
|
|
167
|
+
tags: string[],
|
|
168
|
+
excludeUsername: string | null
|
|
169
|
+
): Promise<Offer | null>;
|
|
170
|
+
|
|
150
171
|
// ===== ICE Candidate Management =====
|
|
151
172
|
|
|
152
173
|
/**
|
|
@@ -177,109 +198,116 @@ export interface Storage {
|
|
|
177
198
|
since?: number
|
|
178
199
|
): Promise<IceCandidate[]>;
|
|
179
200
|
|
|
180
|
-
// ===== Username Management =====
|
|
181
|
-
|
|
182
201
|
/**
|
|
183
|
-
*
|
|
184
|
-
* @param
|
|
185
|
-
* @
|
|
202
|
+
* Retrieves ICE candidates for multiple offers (batch operation)
|
|
203
|
+
* @param offerIds Array of offer identifiers
|
|
204
|
+
* @param username Username requesting the candidates
|
|
205
|
+
* @param since Optional timestamp - only return candidates after this time
|
|
206
|
+
* @returns Map of offer ID to ICE candidates
|
|
186
207
|
*/
|
|
187
|
-
|
|
208
|
+
getIceCandidatesForMultipleOffers(
|
|
209
|
+
offerIds: string[],
|
|
210
|
+
username: string,
|
|
211
|
+
since?: number
|
|
212
|
+
): Promise<Map<string, IceCandidate[]>>;
|
|
213
|
+
|
|
214
|
+
// ===== Credential Management =====
|
|
188
215
|
|
|
189
216
|
/**
|
|
190
|
-
*
|
|
191
|
-
* @param
|
|
192
|
-
* @returns
|
|
217
|
+
* Generates a new credential (random name + secret)
|
|
218
|
+
* @param request Credential generation request
|
|
219
|
+
* @returns Created credential record
|
|
193
220
|
*/
|
|
194
|
-
|
|
221
|
+
generateCredentials(request: GenerateCredentialsRequest): Promise<Credential>;
|
|
195
222
|
|
|
196
223
|
/**
|
|
197
|
-
*
|
|
198
|
-
* @param
|
|
199
|
-
* @returns
|
|
224
|
+
* Gets a credential by name
|
|
225
|
+
* @param name Credential name
|
|
226
|
+
* @returns Credential record if found, null otherwise
|
|
200
227
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// ===== Service Management =====
|
|
228
|
+
getCredential(name: string): Promise<Credential | null>;
|
|
204
229
|
|
|
205
230
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* @
|
|
231
|
+
* Updates credential usage timestamp and expiry
|
|
232
|
+
* Called after successful signature verification
|
|
233
|
+
* @param name Credential name
|
|
234
|
+
* @param lastUsed Last used timestamp
|
|
235
|
+
* @param expiresAt New expiry timestamp
|
|
209
236
|
*/
|
|
210
|
-
|
|
211
|
-
service: Service;
|
|
212
|
-
offers: Offer[];
|
|
213
|
-
}>;
|
|
214
|
-
|
|
237
|
+
updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void>;
|
|
215
238
|
|
|
216
239
|
/**
|
|
217
|
-
*
|
|
218
|
-
* @param
|
|
219
|
-
* @returns
|
|
240
|
+
* Deletes all expired credentials
|
|
241
|
+
* @param now Current timestamp
|
|
242
|
+
* @returns Number of credentials deleted
|
|
220
243
|
*/
|
|
221
|
-
|
|
244
|
+
deleteExpiredCredentials(now: number): Promise<number>;
|
|
245
|
+
|
|
246
|
+
// ===== Rate Limiting =====
|
|
222
247
|
|
|
223
248
|
/**
|
|
224
|
-
*
|
|
225
|
-
* @param
|
|
226
|
-
* @
|
|
249
|
+
* Check and increment rate limit for an identifier
|
|
250
|
+
* @param identifier Unique identifier (e.g., IP address)
|
|
251
|
+
* @param limit Maximum count allowed
|
|
252
|
+
* @param windowMs Time window in milliseconds
|
|
253
|
+
* @returns true if allowed, false if rate limit exceeded
|
|
227
254
|
*/
|
|
228
|
-
|
|
255
|
+
checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean>;
|
|
229
256
|
|
|
230
257
|
/**
|
|
231
|
-
*
|
|
232
|
-
* @param
|
|
233
|
-
* @returns
|
|
258
|
+
* Deletes all expired rate limit entries
|
|
259
|
+
* @param now Current timestamp
|
|
260
|
+
* @returns Number of entries deleted
|
|
234
261
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
deleteExpiredRateLimits(now: number): Promise<number>;
|
|
237
263
|
|
|
264
|
+
// ===== Nonce Tracking (Replay Protection) =====
|
|
238
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Check if nonce has been used and mark it as used (atomic operation)
|
|
268
|
+
* @param nonceKey Unique nonce identifier (format: "nonce:{name}:{nonce}")
|
|
269
|
+
* @param expiresAt Timestamp when nonce expires (should be timestamp + timestampMaxAge)
|
|
270
|
+
* @returns true if nonce is new (allowed), false if already used (replay attack)
|
|
271
|
+
*/
|
|
272
|
+
checkAndMarkNonce(nonceKey: string, expiresAt: number): Promise<boolean>;
|
|
239
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Deletes all expired nonce entries
|
|
276
|
+
* @param now Current timestamp
|
|
277
|
+
* @returns Number of entries deleted
|
|
278
|
+
*/
|
|
279
|
+
deleteExpiredNonces(now: number): Promise<number>;
|
|
240
280
|
|
|
241
281
|
/**
|
|
242
|
-
*
|
|
243
|
-
* Returns unique available offers (where answerer_peer_id IS NULL)
|
|
244
|
-
* @param serviceName Service name (e.g., 'chat')
|
|
245
|
-
* @param version Version string for semver matching (e.g., '1.0.0')
|
|
246
|
-
* @param limit Maximum number of unique services to return
|
|
247
|
-
* @param offset Number of services to skip
|
|
248
|
-
* @returns Array of services with available offers
|
|
282
|
+
* Closes the storage connection and releases resources
|
|
249
283
|
*/
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
limit: number,
|
|
254
|
-
offset: number
|
|
255
|
-
): Promise<Service[]>;
|
|
284
|
+
close(): Promise<void>;
|
|
285
|
+
|
|
286
|
+
// ===== Count Methods (for resource limits) =====
|
|
256
287
|
|
|
257
288
|
/**
|
|
258
|
-
* Gets
|
|
259
|
-
*
|
|
260
|
-
* @param serviceName Service name (e.g., 'chat')
|
|
261
|
-
* @param version Version string for semver matching (e.g., '1.0.0')
|
|
262
|
-
* @returns Random service with available offer, or null if none found
|
|
289
|
+
* Gets total number of offers in storage
|
|
290
|
+
* @returns Total offer count
|
|
263
291
|
*/
|
|
264
|
-
|
|
292
|
+
getOfferCount(): Promise<number>;
|
|
265
293
|
|
|
266
294
|
/**
|
|
267
|
-
*
|
|
268
|
-
* @param
|
|
269
|
-
* @
|
|
270
|
-
* @returns true if deleted, false if not found or not owned
|
|
295
|
+
* Gets number of offers for a specific user
|
|
296
|
+
* @param username Username identifier
|
|
297
|
+
* @returns Offer count for user
|
|
271
298
|
*/
|
|
272
|
-
|
|
299
|
+
getOfferCountByUsername(username: string): Promise<number>;
|
|
273
300
|
|
|
274
301
|
/**
|
|
275
|
-
*
|
|
276
|
-
* @
|
|
277
|
-
* @returns Number of services deleted
|
|
302
|
+
* Gets total number of credentials in storage
|
|
303
|
+
* @returns Total credential count
|
|
278
304
|
*/
|
|
279
|
-
|
|
305
|
+
getCredentialCount(): Promise<number>;
|
|
280
306
|
|
|
281
307
|
/**
|
|
282
|
-
*
|
|
308
|
+
* Gets number of ICE candidates for a specific offer
|
|
309
|
+
* @param offerId Offer identifier
|
|
310
|
+
* @returns ICE candidate count for offer
|
|
283
311
|
*/
|
|
284
|
-
|
|
312
|
+
getIceCandidateCount(offerId: string): Promise<number>;
|
|
285
313
|
}
|
package/src/worker.ts
CHANGED
|
@@ -1,66 +1,47 @@
|
|
|
1
1
|
import { createApp } from './app.ts';
|
|
2
2
|
import { D1Storage } from './storage/d1.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { buildWorkerConfig, runCleanup } from './config.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Cloudflare Workers environment bindings
|
|
7
7
|
*/
|
|
8
8
|
export interface Env {
|
|
9
9
|
DB: D1Database;
|
|
10
|
+
MASTER_ENCRYPTION_KEY: string;
|
|
10
11
|
OFFER_DEFAULT_TTL?: string;
|
|
11
12
|
OFFER_MAX_TTL?: string;
|
|
12
13
|
OFFER_MIN_TTL?: string;
|
|
13
14
|
MAX_OFFERS_PER_REQUEST?: string;
|
|
15
|
+
MAX_BATCH_SIZE?: string;
|
|
14
16
|
CORS_ORIGINS?: string;
|
|
15
17
|
VERSION?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
* Cloudflare Workers fetch handler
|
|
20
|
-
*/
|
|
21
20
|
export default {
|
|
22
21
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Build config from environment
|
|
27
|
-
const config: Config = {
|
|
28
|
-
port: 0, // Not used in Workers
|
|
29
|
-
storageType: 'sqlite', // D1 is SQLite-compatible
|
|
30
|
-
storagePath: '', // Not used with D1
|
|
31
|
-
corsOrigins: env.CORS_ORIGINS
|
|
32
|
-
? env.CORS_ORIGINS.split(',').map(o => o.trim())
|
|
33
|
-
: ['*'],
|
|
34
|
-
version: env.VERSION || 'unknown',
|
|
35
|
-
offerDefaultTtl: env.OFFER_DEFAULT_TTL ? parseInt(env.OFFER_DEFAULT_TTL, 10) : 60000,
|
|
36
|
-
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
|
|
37
|
-
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
|
|
38
|
-
cleanupInterval: 60000, // Not used in Workers (scheduled handler instead)
|
|
39
|
-
maxOffersPerRequest: env.MAX_OFFERS_PER_REQUEST ? parseInt(env.MAX_OFFERS_PER_REQUEST, 10) : 100
|
|
40
|
-
};
|
|
22
|
+
if (!env.MASTER_ENCRYPTION_KEY || env.MASTER_ENCRYPTION_KEY.length !== 64) {
|
|
23
|
+
return new Response('MASTER_ENCRYPTION_KEY must be 64-char hex string', { status: 500 });
|
|
24
|
+
}
|
|
41
25
|
|
|
42
|
-
|
|
26
|
+
const storage = new D1Storage(env.DB, env.MASTER_ENCRYPTION_KEY);
|
|
27
|
+
const config = buildWorkerConfig(env);
|
|
43
28
|
const app = createApp(storage, config);
|
|
44
29
|
|
|
45
|
-
// Handle request
|
|
46
30
|
return app.fetch(request, env, ctx);
|
|
47
31
|
},
|
|
48
32
|
|
|
49
|
-
/**
|
|
50
|
-
* Scheduled handler for cron triggers
|
|
51
|
-
* Runs periodically to clean up expired offers
|
|
52
|
-
*/
|
|
53
33
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
54
|
-
const storage = new D1Storage(env.DB);
|
|
34
|
+
const storage = new D1Storage(env.DB, env.MASTER_ENCRYPTION_KEY);
|
|
55
35
|
const now = Date.now();
|
|
56
36
|
|
|
57
37
|
try {
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
const result = await runCleanup(storage, now);
|
|
39
|
+
const total = result.offers + result.credentials + result.rateLimits + result.nonces;
|
|
40
|
+
if (total > 0) {
|
|
41
|
+
console.log(`Cleanup: ${result.offers} offers, ${result.credentials} credentials, ${result.rateLimits} rate limits, ${result.nonces} nonces`);
|
|
42
|
+
}
|
|
62
43
|
} catch (error) {
|
|
63
|
-
console.error('
|
|
44
|
+
console.error('Cleanup error:', error);
|
|
64
45
|
}
|
|
65
46
|
},
|
|
66
47
|
};
|