@xtr-dev/rondevu-server 0.5.1 → 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.
@@ -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
45
+ export interface Credential {
46
+ name: string; // Random name (e.g., "brave-tiger-7a3f")
47
+ secret: string; // Random secret (API key style)
48
+ createdAt: number;
49
+ expiresAt: number; // 365 days from creation/last use
51
50
  lastUsed: number;
52
- metadata?: string; // JSON optional user metadata
53
51
  }
54
52
 
55
53
  /**
56
- * Request to claim a username
54
+ * Request to generate new credentials
57
55
  */
58
- export interface ClaimUsernameRequest {
59
- username: string;
60
- publicKey: string;
61
- signature: string;
62
- message: string; // "claim:{username}:{timestamp}"
56
+ export interface GenerateCredentialsRequest {
57
+ name?: string; // Optional: claim specific username (must be unique, 4-32 chars)
58
+ expiresAt?: number; // Optional: override default expiry
63
59
  }
64
60
 
65
61
  /**
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
75
- createdAt: number;
76
- expiresAt: number;
77
- }
78
-
79
- /**
80
- * Request to create a single service
81
- */
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
86
- }
87
-
88
- /**
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,106 +198,85 @@ export interface Storage {
177
198
  since?: number
178
199
  ): Promise<IceCandidate[]>;
179
200
 
180
- // ===== Username Management =====
181
-
182
- /**
183
- * Claims a username (or refreshes expiry if already owned)
184
- * @param request Username claim request with signature
185
- * @returns Created/updated username record
186
- */
187
- claimUsername(request: ClaimUsernameRequest): Promise<Username>;
188
-
189
201
  /**
190
- * Gets a username record
191
- * @param username Username to look up
192
- * @returns Username record if claimed, null otherwise
193
- */
194
- getUsername(username: string): Promise<Username | null>;
195
-
196
- /**
197
- * Deletes all expired usernames
198
- * @param now Current timestamp
199
- * @returns Number of usernames deleted
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
200
207
  */
201
- deleteExpiredUsernames(now: number): Promise<number>;
208
+ getIceCandidatesForMultipleOffers(
209
+ offerIds: string[],
210
+ username: string,
211
+ since?: number
212
+ ): Promise<Map<string, IceCandidate[]>>;
202
213
 
203
- // ===== Service Management =====
214
+ // ===== Credential Management =====
204
215
 
205
216
  /**
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
217
+ * Generates a new credential (random name + secret)
218
+ * @param request Credential generation request
219
+ * @returns Created credential record
209
220
  */
210
- createService(request: CreateServiceRequest): Promise<{
211
- service: Service;
212
- offers: Offer[];
213
- }>;
214
-
221
+ generateCredentials(request: GenerateCredentialsRequest): Promise<Credential>;
215
222
 
216
223
  /**
217
- * Gets all offers for a service
218
- * @param serviceId Service ID
219
- * @returns Array of offers for the service
224
+ * Gets a credential by name
225
+ * @param name Credential name
226
+ * @returns Credential record if found, null otherwise
220
227
  */
221
- getOffersForService(serviceId: string): Promise<Offer[]>;
228
+ getCredential(name: string): Promise<Credential | null>;
222
229
 
223
230
  /**
224
- * Gets a service by its service ID
225
- * @param serviceId Service ID
226
- * @returns Service if found, null otherwise
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
227
236
  */
228
- getServiceById(serviceId: string): Promise<Service | null>;
237
+ updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void>;
229
238
 
230
239
  /**
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
240
+ * Deletes all expired credentials
241
+ * @param now Current timestamp
242
+ * @returns Number of credentials deleted
234
243
  */
235
- getServiceByFqn(serviceFqn: string): Promise<Service | null>;
236
-
237
-
238
-
244
+ deleteExpiredCredentials(now: number): Promise<number>;
239
245
 
246
+ // ===== Rate Limiting =====
240
247
 
241
248
  /**
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
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
249
254
  */
250
- discoverServices(
251
- serviceName: string,
252
- version: string,
253
- limit: number,
254
- offset: number
255
- ): Promise<Service[]>;
255
+ checkRateLimit(identifier: string, limit: number, windowMs: number): Promise<boolean>;
256
256
 
257
257
  /**
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
258
+ * Deletes all expired rate limit entries
259
+ * @param now Current timestamp
260
+ * @returns Number of entries deleted
263
261
  */
264
- getRandomService(serviceName: string, version: string): Promise<Service | null>;
262
+ deleteExpiredRateLimits(now: number): Promise<number>;
263
+
264
+ // ===== Nonce Tracking (Replay Protection) =====
265
265
 
266
266
  /**
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
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
271
  */
272
- deleteService(serviceId: string, username: string): Promise<boolean>;
272
+ checkAndMarkNonce(nonceKey: string, expiresAt: number): Promise<boolean>;
273
273
 
274
274
  /**
275
- * Deletes all expired services
275
+ * Deletes all expired nonce entries
276
276
  * @param now Current timestamp
277
- * @returns Number of services deleted
277
+ * @returns Number of entries deleted
278
278
  */
279
- deleteExpiredServices(now: number): Promise<number>;
279
+ deleteExpiredNonces(now: number): Promise<number>;
280
280
 
281
281
  /**
282
282
  * Closes the storage connection and releases resources
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
  };