@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.
@@ -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
- serviceId?: string; // Optional link to service (null for standalone offers)
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
- serviceId?: string; // Optional link to service
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 claimed username with cryptographic proof
42
+ * Represents a credential (random name + secret pair)
43
+ * Replaces the old username/publicKey system for simpler authentication
45
44
  */
46
- export interface Username {
47
- username: string;
48
- publicKey: string; // Base64-encoded Ed25519 public key
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 create a single service
54
+ * Request to generate new credentials
81
55
  */
82
- export interface CreateServiceRequest {
83
- serviceFqn: string; // Full FQN with username: chat:1.0.0@alice
84
- expiresAt: number;
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 DNS-like system
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
- * Claims a username (or refreshes expiry if already owned)
184
- * @param request Username claim request with signature
185
- * @returns Created/updated username record
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
- claimUsername(request: ClaimUsernameRequest): Promise<Username>;
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
- * Gets a username record
191
- * @param username Username to look up
192
- * @returns Username record if claimed, null otherwise
217
+ * Generates a new credential (random name + secret)
218
+ * @param request Credential generation request
219
+ * @returns Created credential record
193
220
  */
194
- getUsername(username: string): Promise<Username | null>;
221
+ generateCredentials(request: GenerateCredentialsRequest): Promise<Credential>;
195
222
 
196
223
  /**
197
- * Deletes all expired usernames
198
- * @param now Current timestamp
199
- * @returns Number of usernames deleted
224
+ * Gets a credential by name
225
+ * @param name Credential name
226
+ * @returns Credential record if found, null otherwise
200
227
  */
201
- deleteExpiredUsernames(now: number): Promise<number>;
202
-
203
- // ===== Service Management =====
228
+ getCredential(name: string): Promise<Credential | null>;
204
229
 
205
230
  /**
206
- * Creates a new service with offers
207
- * @param request Service creation request (includes offers)
208
- * @returns Created service with generated ID and created offers
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
- createService(request: CreateServiceRequest): Promise<{
211
- service: Service;
212
- offers: Offer[];
213
- }>;
214
-
237
+ updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void>;
215
238
 
216
239
  /**
217
- * Gets all offers for a service
218
- * @param serviceId Service ID
219
- * @returns Array of offers for the service
240
+ * Deletes all expired credentials
241
+ * @param now Current timestamp
242
+ * @returns Number of credentials deleted
220
243
  */
221
- getOffersForService(serviceId: string): Promise<Offer[]>;
244
+ deleteExpiredCredentials(now: number): Promise<number>;
245
+
246
+ // ===== Rate Limiting =====
222
247
 
223
248
  /**
224
- * Gets a service by its service ID
225
- * @param serviceId Service ID
226
- * @returns Service if found, null otherwise
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
- getServiceById(serviceId: string): Promise<Service | null>;
255
+ checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean>;
229
256
 
230
257
  /**
231
- * Gets a service by its fully qualified name (FQN)
232
- * @param serviceFqn Full service FQN (e.g., "chat:1.0.0@alice")
233
- * @returns Service if found, null otherwise
258
+ * Deletes all expired rate limit entries
259
+ * @param now Current timestamp
260
+ * @returns Number of entries deleted
234
261
  */
235
- getServiceByFqn(serviceFqn: string): Promise<Service | null>;
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
- * Discovers services by name and version with pagination
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
- discoverServices(
251
- serviceName: string,
252
- version: string,
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 a random available service by name and version
259
- * Returns a single random offer that is available (answerer_peer_id IS NULL)
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
- getRandomService(serviceName: string, version: string): Promise<Service | null>;
292
+ getOfferCount(): Promise<number>;
265
293
 
266
294
  /**
267
- * Deletes a service (with ownership verification)
268
- * @param serviceId Service ID
269
- * @param username Owner username (for verification)
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
- deleteService(serviceId: string, username: string): Promise<boolean>;
299
+ getOfferCountByUsername(username: string): Promise<number>;
273
300
 
274
301
  /**
275
- * Deletes all expired services
276
- * @param now Current timestamp
277
- * @returns Number of services deleted
302
+ * Gets total number of credentials in storage
303
+ * @returns Total credential count
278
304
  */
279
- deleteExpiredServices(now: number): Promise<number>;
305
+ getCredentialCount(): Promise<number>;
280
306
 
281
307
  /**
282
- * Closes the storage connection and releases resources
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
- close(): Promise<void>;
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 { Config } from './config.ts';
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
- // Initialize D1 storage
24
- const storage = new D1Storage(env.DB);
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
- // Create Hono app
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
- // Delete expired offers
59
- const deletedCount = await storage.deleteExpiredOffers(now);
60
-
61
- console.log(`Cleaned up ${deletedCount} expired offers at ${new Date(now).toISOString()}`);
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('Error cleaning up offers:', error);
44
+ console.error('Cleanup error:', error);
64
45
  }
65
46
  },
66
47
  };