@xtr-dev/rondevu-server 0.3.0 → 0.5.0
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/ADVANCED.md +502 -0
- package/README.md +139 -251
- package/dist/index.js +715 -770
- package/dist/index.js.map +4 -4
- package/migrations/0006_service_offer_refactor.sql +40 -0
- package/migrations/0007_simplify_schema.sql +54 -0
- package/migrations/0008_peer_id_to_username.sql +67 -0
- package/migrations/fresh_schema.sql +81 -0
- package/package.json +2 -1
- package/src/app.ts +38 -677
- package/src/config.ts +0 -13
- package/src/crypto.ts +98 -133
- package/src/rpc.ts +725 -0
- package/src/storage/d1.ts +169 -182
- package/src/storage/sqlite.ts +142 -168
- package/src/storage/types.ts +51 -95
- package/src/worker.ts +0 -6
- package/wrangler.toml +3 -3
- package/src/middleware/auth.ts +0 -51
package/src/storage/types.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export interface Offer {
|
|
5
5
|
id: string;
|
|
6
|
-
|
|
6
|
+
username: string;
|
|
7
7
|
serviceId?: string; // Optional link to service (null for standalone offers)
|
|
8
|
+
serviceFqn?: string; // Denormalized service FQN for easier queries
|
|
8
9
|
sdp: string;
|
|
9
10
|
createdAt: number;
|
|
10
11
|
expiresAt: number;
|
|
11
12
|
lastSeen: number;
|
|
12
|
-
|
|
13
|
-
answererPeerId?: string;
|
|
13
|
+
answererUsername?: string;
|
|
14
14
|
answerSdp?: string;
|
|
15
15
|
answeredAt?: number;
|
|
16
16
|
}
|
|
@@ -22,7 +22,7 @@ export interface Offer {
|
|
|
22
22
|
export interface IceCandidate {
|
|
23
23
|
id: number;
|
|
24
24
|
offerId: string;
|
|
25
|
-
|
|
25
|
+
username: string;
|
|
26
26
|
role: 'offerer' | 'answerer';
|
|
27
27
|
candidate: any; // Full candidate object as JSON - don't enforce structure
|
|
28
28
|
createdAt: number;
|
|
@@ -33,11 +33,11 @@ export interface IceCandidate {
|
|
|
33
33
|
*/
|
|
34
34
|
export interface CreateOfferRequest {
|
|
35
35
|
id?: string;
|
|
36
|
-
|
|
36
|
+
username: string;
|
|
37
37
|
serviceId?: string; // Optional link to service
|
|
38
|
+
serviceFqn?: string; // Optional service FQN
|
|
38
39
|
sdp: string;
|
|
39
40
|
expiresAt: number;
|
|
40
|
-
secret?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -64,58 +64,27 @@ export interface ClaimUsernameRequest {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Represents a published service (can have multiple offers)
|
|
67
|
+
* New format: service:version@username (e.g., chat:1.0.0@alice)
|
|
67
68
|
*/
|
|
68
69
|
export interface Service {
|
|
69
70
|
id: string; // UUID v4
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
72
75
|
createdAt: number;
|
|
73
76
|
expiresAt: number;
|
|
74
|
-
isPublic: boolean;
|
|
75
|
-
metadata?: string; // JSON service description
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
80
|
* Request to create a single service
|
|
80
81
|
*/
|
|
81
82
|
export interface CreateServiceRequest {
|
|
82
|
-
|
|
83
|
-
serviceFqn: string;
|
|
83
|
+
serviceFqn: string; // Full FQN with username: chat:1.0.0@alice
|
|
84
84
|
expiresAt: number;
|
|
85
|
-
isPublic?: boolean;
|
|
86
|
-
metadata?: string;
|
|
87
85
|
offers: CreateOfferRequest[]; // Multiple offers per service
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
/**
|
|
91
|
-
* Request to create multiple services in batch
|
|
92
|
-
*/
|
|
93
|
-
export interface BatchCreateServicesRequest {
|
|
94
|
-
services: CreateServiceRequest[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Represents a service index entry (privacy layer)
|
|
99
|
-
*/
|
|
100
|
-
export interface ServiceIndex {
|
|
101
|
-
uuid: string; // Random UUID for privacy
|
|
102
|
-
serviceId: string;
|
|
103
|
-
username: string;
|
|
104
|
-
serviceFqn: string;
|
|
105
|
-
createdAt: number;
|
|
106
|
-
expiresAt: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Service info for discovery (privacy-aware)
|
|
111
|
-
*/
|
|
112
|
-
export interface ServiceInfo {
|
|
113
|
-
uuid: string;
|
|
114
|
-
isPublic: boolean;
|
|
115
|
-
serviceFqn?: string; // Only present if public
|
|
116
|
-
metadata?: string; // Only present if public
|
|
117
|
-
}
|
|
118
|
-
|
|
119
88
|
/**
|
|
120
89
|
* Storage interface for rondevu DNS-like system
|
|
121
90
|
* Implementations can use different backends (SQLite, D1, etc.)
|
|
@@ -131,11 +100,11 @@ export interface Storage {
|
|
|
131
100
|
createOffers(offers: CreateOfferRequest[]): Promise<Offer[]>;
|
|
132
101
|
|
|
133
102
|
/**
|
|
134
|
-
* Retrieves all offers from a specific
|
|
135
|
-
* @param
|
|
136
|
-
* @returns Array of offers from the
|
|
103
|
+
* Retrieves all offers from a specific user
|
|
104
|
+
* @param username Username identifier
|
|
105
|
+
* @returns Array of offers from the user
|
|
137
106
|
*/
|
|
138
|
-
|
|
107
|
+
getOffersByUsername(username: string): Promise<Offer[]>;
|
|
139
108
|
|
|
140
109
|
/**
|
|
141
110
|
* Retrieves a specific offer by ID
|
|
@@ -147,10 +116,10 @@ export interface Storage {
|
|
|
147
116
|
/**
|
|
148
117
|
* Deletes an offer (with ownership verification)
|
|
149
118
|
* @param offerId Offer identifier
|
|
150
|
-
* @param
|
|
119
|
+
* @param ownerUsername Username of the owner (for verification)
|
|
151
120
|
* @returns true if deleted, false if not found or not owned
|
|
152
121
|
*/
|
|
153
|
-
deleteOffer(offerId: string,
|
|
122
|
+
deleteOffer(offerId: string, ownerUsername: string): Promise<boolean>;
|
|
154
123
|
|
|
155
124
|
/**
|
|
156
125
|
* Deletes all expired offers
|
|
@@ -162,36 +131,35 @@ export interface Storage {
|
|
|
162
131
|
/**
|
|
163
132
|
* Answers an offer (locks it to the answerer)
|
|
164
133
|
* @param offerId Offer identifier
|
|
165
|
-
* @param
|
|
134
|
+
* @param answererUsername Answerer's username
|
|
166
135
|
* @param answerSdp WebRTC answer SDP
|
|
167
|
-
* @param secret Optional secret for protected offers
|
|
168
136
|
* @returns Success status and optional error message
|
|
169
137
|
*/
|
|
170
|
-
answerOffer(offerId: string,
|
|
138
|
+
answerOffer(offerId: string, answererUsername: string, answerSdp: string): Promise<{
|
|
171
139
|
success: boolean;
|
|
172
140
|
error?: string;
|
|
173
141
|
}>;
|
|
174
142
|
|
|
175
143
|
/**
|
|
176
144
|
* Retrieves all answered offers for a specific offerer
|
|
177
|
-
* @param
|
|
145
|
+
* @param offererUsername Offerer's username
|
|
178
146
|
* @returns Array of answered offers
|
|
179
147
|
*/
|
|
180
|
-
getAnsweredOffers(
|
|
148
|
+
getAnsweredOffers(offererUsername: string): Promise<Offer[]>;
|
|
181
149
|
|
|
182
150
|
// ===== ICE Candidate Management =====
|
|
183
151
|
|
|
184
152
|
/**
|
|
185
153
|
* Adds ICE candidates for an offer
|
|
186
154
|
* @param offerId Offer identifier
|
|
187
|
-
* @param
|
|
188
|
-
* @param role Role of the
|
|
155
|
+
* @param username Username posting the candidates
|
|
156
|
+
* @param role Role of the user (offerer or answerer)
|
|
189
157
|
* @param candidates Array of candidate objects (stored as plain JSON)
|
|
190
158
|
* @returns Number of candidates added
|
|
191
159
|
*/
|
|
192
160
|
addIceCandidates(
|
|
193
161
|
offerId: string,
|
|
194
|
-
|
|
162
|
+
username: string,
|
|
195
163
|
role: 'offerer' | 'answerer',
|
|
196
164
|
candidates: any[]
|
|
197
165
|
): Promise<number>;
|
|
@@ -225,13 +193,6 @@ export interface Storage {
|
|
|
225
193
|
*/
|
|
226
194
|
getUsername(username: string): Promise<Username | null>;
|
|
227
195
|
|
|
228
|
-
/**
|
|
229
|
-
* Updates the last_used timestamp for a username (extends expiry)
|
|
230
|
-
* @param username Username to update
|
|
231
|
-
* @returns true if updated, false if not found
|
|
232
|
-
*/
|
|
233
|
-
touchUsername(username: string): Promise<boolean>;
|
|
234
|
-
|
|
235
196
|
/**
|
|
236
197
|
* Deletes all expired usernames
|
|
237
198
|
* @param now Current timestamp
|
|
@@ -244,24 +205,13 @@ export interface Storage {
|
|
|
244
205
|
/**
|
|
245
206
|
* Creates a new service with offers
|
|
246
207
|
* @param request Service creation request (includes offers)
|
|
247
|
-
* @returns Created service with generated ID
|
|
208
|
+
* @returns Created service with generated ID and created offers
|
|
248
209
|
*/
|
|
249
210
|
createService(request: CreateServiceRequest): Promise<{
|
|
250
211
|
service: Service;
|
|
251
|
-
indexUuid: string;
|
|
252
212
|
offers: Offer[];
|
|
253
213
|
}>;
|
|
254
214
|
|
|
255
|
-
/**
|
|
256
|
-
* Creates multiple services with offers in batch
|
|
257
|
-
* @param requests Array of service creation requests
|
|
258
|
-
* @returns Array of created services with IDs, UUIDs, and offers
|
|
259
|
-
*/
|
|
260
|
-
batchCreateServices(requests: CreateServiceRequest[]): Promise<Array<{
|
|
261
|
-
service: Service;
|
|
262
|
-
indexUuid: string;
|
|
263
|
-
offers: Offer[];
|
|
264
|
-
}>>;
|
|
265
215
|
|
|
266
216
|
/**
|
|
267
217
|
* Gets all offers for a service
|
|
@@ -278,34 +228,40 @@ export interface Storage {
|
|
|
278
228
|
getServiceById(serviceId: string): Promise<Service | null>;
|
|
279
229
|
|
|
280
230
|
/**
|
|
281
|
-
* Gets a service by its
|
|
282
|
-
* @param
|
|
231
|
+
* Gets a service by its fully qualified name (FQN)
|
|
232
|
+
* @param serviceFqn Full service FQN (e.g., "chat:1.0.0@alice")
|
|
283
233
|
* @returns Service if found, null otherwise
|
|
284
234
|
*/
|
|
285
|
-
|
|
235
|
+
getServiceByFqn(serviceFqn: string): Promise<Service | null>;
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
286
239
|
|
|
287
|
-
/**
|
|
288
|
-
* Lists all services for a username (with privacy filtering)
|
|
289
|
-
* @param username Username to query
|
|
290
|
-
* @returns Array of service info (UUIDs only for private services)
|
|
291
|
-
*/
|
|
292
|
-
listServicesForUsername(username: string): Promise<ServiceInfo[]>;
|
|
293
240
|
|
|
294
241
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* @param
|
|
298
|
-
* @
|
|
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
|
|
299
249
|
*/
|
|
300
|
-
|
|
250
|
+
discoverServices(
|
|
251
|
+
serviceName: string,
|
|
252
|
+
version: string,
|
|
253
|
+
limit: number,
|
|
254
|
+
offset: number
|
|
255
|
+
): Promise<Service[]>;
|
|
301
256
|
|
|
302
257
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* @param serviceName Service name (e.g., '
|
|
306
|
-
* @
|
|
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
|
|
307
263
|
*/
|
|
308
|
-
|
|
264
|
+
getRandomService(serviceName: string, version: string): Promise<Service | null>;
|
|
309
265
|
|
|
310
266
|
/**
|
|
311
267
|
* Deletes a service (with ownership verification)
|
package/src/worker.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createApp } from './app.ts';
|
|
2
2
|
import { D1Storage } from './storage/d1.ts';
|
|
3
|
-
import { generateSecretKey } from './crypto.ts';
|
|
4
3
|
import { Config } from './config.ts';
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -8,7 +7,6 @@ import { Config } from './config.ts';
|
|
|
8
7
|
*/
|
|
9
8
|
export interface Env {
|
|
10
9
|
DB: D1Database;
|
|
11
|
-
AUTH_SECRET?: string;
|
|
12
10
|
OFFER_DEFAULT_TTL?: string;
|
|
13
11
|
OFFER_MAX_TTL?: string;
|
|
14
12
|
OFFER_MIN_TTL?: string;
|
|
@@ -25,9 +23,6 @@ export default {
|
|
|
25
23
|
// Initialize D1 storage
|
|
26
24
|
const storage = new D1Storage(env.DB);
|
|
27
25
|
|
|
28
|
-
// Generate or use provided auth secret
|
|
29
|
-
const authSecret = env.AUTH_SECRET || generateSecretKey();
|
|
30
|
-
|
|
31
26
|
// Build config from environment
|
|
32
27
|
const config: Config = {
|
|
33
28
|
port: 0, // Not used in Workers
|
|
@@ -37,7 +32,6 @@ export default {
|
|
|
37
32
|
? env.CORS_ORIGINS.split(',').map(o => o.trim())
|
|
38
33
|
: ['*'],
|
|
39
34
|
version: env.VERSION || 'unknown',
|
|
40
|
-
authSecret,
|
|
41
35
|
offerDefaultTtl: env.OFFER_DEFAULT_TTL ? parseInt(env.OFFER_DEFAULT_TTL, 10) : 60000,
|
|
42
36
|
offerMaxTtl: env.OFFER_MAX_TTL ? parseInt(env.OFFER_MAX_TTL, 10) : 86400000,
|
|
43
37
|
offerMinTtl: env.OFFER_MIN_TTL ? parseInt(env.OFFER_MIN_TTL, 10) : 60000,
|
package/wrangler.toml
CHANGED
|
@@ -7,7 +7,7 @@ compatibility_flags = ["nodejs_compat"]
|
|
|
7
7
|
[[d1_databases]]
|
|
8
8
|
binding = "DB"
|
|
9
9
|
database_name = "rondevu-offers"
|
|
10
|
-
database_id = "
|
|
10
|
+
database_id = "3d469855-d37f-477b-b139-fa58843a54ff"
|
|
11
11
|
|
|
12
12
|
# Environment variables
|
|
13
13
|
[vars]
|
|
@@ -17,7 +17,7 @@ OFFER_MIN_TTL = "60000" # Min offer TTL: 1 minute
|
|
|
17
17
|
MAX_OFFERS_PER_REQUEST = "100" # Max offers per request
|
|
18
18
|
MAX_TOPICS_PER_OFFER = "50" # Max topics per offer
|
|
19
19
|
CORS_ORIGINS = "*" # Comma-separated list of allowed origins
|
|
20
|
-
VERSION = "0.
|
|
20
|
+
VERSION = "0.4.0" # Semantic version
|
|
21
21
|
|
|
22
22
|
# AUTH_SECRET should be set as a secret, not a var
|
|
23
23
|
# Run: npx wrangler secret put AUTH_SECRET
|
|
@@ -39,7 +39,7 @@ command = ""
|
|
|
39
39
|
|
|
40
40
|
[observability]
|
|
41
41
|
[observability.logs]
|
|
42
|
-
enabled =
|
|
42
|
+
enabled = true
|
|
43
43
|
head_sampling_rate = 1
|
|
44
44
|
invocation_logs = true
|
|
45
45
|
persist = true
|
package/src/middleware/auth.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { Context, Next } from 'hono';
|
|
2
|
-
import { validateCredentials } from '../crypto.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Authentication middleware for Rondevu
|
|
6
|
-
* Validates Bearer token in format: {peerId}:{encryptedSecret}
|
|
7
|
-
*/
|
|
8
|
-
export function createAuthMiddleware(authSecret: string) {
|
|
9
|
-
return async (c: Context, next: Next) => {
|
|
10
|
-
const authHeader = c.req.header('Authorization');
|
|
11
|
-
|
|
12
|
-
if (!authHeader) {
|
|
13
|
-
return c.json({ error: 'Missing Authorization header' }, 401);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Expect format: Bearer {peerId}:{secret}
|
|
17
|
-
const parts = authHeader.split(' ');
|
|
18
|
-
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
|
19
|
-
return c.json({ error: 'Invalid Authorization header format. Expected: Bearer {peerId}:{secret}' }, 401);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const credentials = parts[1].split(':');
|
|
23
|
-
if (credentials.length !== 2) {
|
|
24
|
-
return c.json({ error: 'Invalid credentials format. Expected: {peerId}:{secret}' }, 401);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const [peerId, encryptedSecret] = credentials;
|
|
28
|
-
|
|
29
|
-
// Validate credentials (async operation)
|
|
30
|
-
const isValid = await validateCredentials(peerId, encryptedSecret, authSecret);
|
|
31
|
-
if (!isValid) {
|
|
32
|
-
return c.json({ error: 'Invalid credentials' }, 401);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Attach peer ID to context for use in handlers
|
|
36
|
-
c.set('peerId', peerId);
|
|
37
|
-
|
|
38
|
-
await next();
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Helper to get authenticated peer ID from context
|
|
44
|
-
*/
|
|
45
|
-
export function getAuthenticatedPeerId(c: Context): string {
|
|
46
|
-
const peerId = c.get('peerId');
|
|
47
|
-
if (!peerId) {
|
|
48
|
-
throw new Error('No authenticated peer ID in context');
|
|
49
|
-
}
|
|
50
|
-
return peerId;
|
|
51
|
-
}
|