@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.
- 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 +361 -263
- 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
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
|
-
|
|
50
|
-
expiresAt: number; // 365 days from
|
|
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
|
|
54
|
+
* Request to generate new credentials
|
|
57
55
|
*/
|
|
58
|
-
export interface
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
191
|
-
* @param
|
|
192
|
-
* @
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
208
|
+
getIceCandidatesForMultipleOffers(
|
|
209
|
+
offerIds: string[],
|
|
210
|
+
username: string,
|
|
211
|
+
since?: number
|
|
212
|
+
): Promise<Map<string, IceCandidate[]>>;
|
|
202
213
|
|
|
203
|
-
// =====
|
|
214
|
+
// ===== Credential Management =====
|
|
204
215
|
|
|
205
216
|
/**
|
|
206
|
-
*
|
|
207
|
-
* @param request
|
|
208
|
-
* @returns Created
|
|
217
|
+
* Generates a new credential (random name + secret)
|
|
218
|
+
* @param request Credential generation request
|
|
219
|
+
* @returns Created credential record
|
|
209
220
|
*/
|
|
210
|
-
|
|
211
|
-
service: Service;
|
|
212
|
-
offers: Offer[];
|
|
213
|
-
}>;
|
|
214
|
-
|
|
221
|
+
generateCredentials(request: GenerateCredentialsRequest): Promise<Credential>;
|
|
215
222
|
|
|
216
223
|
/**
|
|
217
|
-
* Gets
|
|
218
|
-
* @param
|
|
219
|
-
* @returns
|
|
224
|
+
* Gets a credential by name
|
|
225
|
+
* @param name Credential name
|
|
226
|
+
* @returns Credential record if found, null otherwise
|
|
220
227
|
*/
|
|
221
|
-
|
|
228
|
+
getCredential(name: string): Promise<Credential | null>;
|
|
222
229
|
|
|
223
230
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* @
|
|
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
|
-
|
|
237
|
+
updateCredentialUsage(name: string, lastUsed: number, expiresAt: number): Promise<void>;
|
|
229
238
|
|
|
230
239
|
/**
|
|
231
|
-
*
|
|
232
|
-
* @param
|
|
233
|
-
* @returns
|
|
240
|
+
* Deletes all expired credentials
|
|
241
|
+
* @param now Current timestamp
|
|
242
|
+
* @returns Number of credentials deleted
|
|
234
243
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
deleteExpiredCredentials(now: number): Promise<number>;
|
|
239
245
|
|
|
246
|
+
// ===== Rate Limiting =====
|
|
240
247
|
|
|
241
248
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* @param
|
|
245
|
-
* @param
|
|
246
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* @
|
|
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
|
-
|
|
262
|
+
deleteExpiredRateLimits(now: number): Promise<number>;
|
|
263
|
+
|
|
264
|
+
// ===== Nonce Tracking (Replay Protection) =====
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
268
|
-
* @param
|
|
269
|
-
* @param
|
|
270
|
-
* @returns true if
|
|
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
|
-
|
|
272
|
+
checkAndMarkNonce(nonceKey: string, expiresAt: number): Promise<boolean>;
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
|
-
* Deletes all expired
|
|
275
|
+
* Deletes all expired nonce entries
|
|
276
276
|
* @param now Current timestamp
|
|
277
|
-
* @returns Number of
|
|
277
|
+
* @returns Number of entries deleted
|
|
278
278
|
*/
|
|
279
|
-
|
|
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 {
|
|
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
|
};
|