@xtr-dev/rondevu-server 0.5.11 → 0.5.13

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **WebRTC signaling server with tags-based discovery**
6
6
 
7
- HTTP signaling server with credential-based authentication, tag-based offer discovery, and JSON-RPC interface. Multiple storage backends supported.
7
+ HTTP signaling server with stateless Ed25519 authentication, tag-based offer discovery, and JSON-RPC interface. Multiple storage backends supported.
8
8
 
9
9
  ## Quick Start
10
10
 
@@ -57,21 +57,10 @@ npm install pg # for PostgreSQL
57
57
 
58
58
  All API calls go to `POST /rpc` with JSON-RPC format. Requests must be arrays.
59
59
 
60
- ### Generate Credentials
61
-
62
- ```json
63
- [{ "method": "generateCredentials", "params": { "name": "alice" } }]
64
- ```
65
-
66
- Response:
67
- ```json
68
- [{ "success": true, "result": { "name": "alice", "secret": "5a7f3e..." } }]
69
- ```
70
-
71
60
  ### Publish Offer (authenticated)
72
61
 
73
62
  ```
74
- Headers: X-Name, X-Timestamp, X-Nonce, X-Signature
63
+ Headers: X-PublicKey, X-Timestamp, X-Nonce, X-Signature
75
64
  ```
76
65
 
77
66
  ```json
@@ -81,7 +70,7 @@ Headers: X-Name, X-Timestamp, X-Nonce, X-Signature
81
70
  }]
82
71
  ```
83
72
 
84
- ### Discover Offers
73
+ ### Discover Offers (unauthenticated)
85
74
 
86
75
  ```json
87
76
  [{ "method": "discover", "params": { "tags": ["chat"], "limit": 10 } }]
@@ -97,18 +86,20 @@ Headers: X-Name, X-Timestamp, X-Nonce, X-Signature
97
86
 
98
87
  - `addIceCandidates` - Add ICE candidates
99
88
  - `getIceCandidates` - Get ICE candidates
100
- - `poll` - Poll for answers
89
+ - `poll` - Poll for answers and ICE candidates
101
90
  - `deleteOffer` - Delete an offer
102
91
 
103
92
  ## Authentication
104
93
 
105
- Authenticated methods require HMAC-SHA256 signatures:
94
+ **Stateless Ed25519**: No registration required. Generate a keypair locally and sign requests.
106
95
 
107
96
  ```
108
- Message: timestamp:nonce:method:JSON.stringify(params)
109
- Headers: X-Name, X-Timestamp, X-Nonce, X-Signature (base64 HMAC)
97
+ Message: timestamp:nonce:method:canonicalJSON(params)
98
+ Headers: X-PublicKey, X-Timestamp, X-Nonce, X-Signature (base64 Ed25519)
110
99
  ```
111
100
 
101
+ The server verifies signatures directly using the public key from the header - no identity table, no registration step. Your public key IS your identity.
102
+
112
103
  ## Configuration
113
104
 
114
105
  | Variable | Default | Description |
@@ -119,12 +110,9 @@ Headers: X-Name, X-Timestamp, X-Nonce, X-Signature (base64 HMAC)
119
110
  | `DATABASE_URL` | - | Connection string (for `mysql`/`postgres`) |
120
111
  | `DB_POOL_SIZE` | `10` | Connection pool size (for `mysql`/`postgres`) |
121
112
  | `CORS_ORIGINS` | `*` | Allowed origins |
122
- | `MASTER_ENCRYPTION_KEY` | - | 64-char hex for secret encryption |
123
113
  | `OFFER_DEFAULT_TTL` | `60000` | Default offer TTL (ms) |
124
114
  | `OFFER_MAX_TTL` | `86400000` | Max offer TTL (24h) |
125
115
 
126
- Generate encryption key: `openssl rand -hex 32`
127
-
128
116
  ## Tag Validation
129
117
 
130
118
  Tags: 1-64 chars, lowercase alphanumeric with dots/dashes.
package/dist/index.js CHANGED
@@ -469,7 +469,7 @@ var init_memory = __esm({
469
469
  }
470
470
  return count;
471
471
  }
472
- async answerOffer(offerId, answererUsername, answerSdp) {
472
+ async answerOffer(offerId, answererUsername, answerSdp, matchedTags) {
473
473
  const offer = await this.getOfferById(offerId);
474
474
  if (!offer) {
475
475
  return { success: false, error: "Offer not found or expired" };
@@ -481,6 +481,7 @@ var init_memory = __esm({
481
481
  offer.answererUsername = answererUsername;
482
482
  offer.answerSdp = answerSdp;
483
483
  offer.answeredAt = now;
484
+ offer.matchedTags = matchedTags;
484
485
  if (!this.offersByAnswerer.has(answererUsername)) {
485
486
  this.offersByAnswerer.set(answererUsername, /* @__PURE__ */ new Set());
486
487
  }
@@ -815,7 +816,8 @@ var init_sqlite = __esm({
815
816
  last_seen INTEGER NOT NULL,
816
817
  answerer_username TEXT,
817
818
  answer_sdp TEXT,
818
- answered_at INTEGER
819
+ answered_at INTEGER,
820
+ matched_tags TEXT
819
821
  );
820
822
 
821
823
  CREATE INDEX IF NOT EXISTS idx_offers_username ON offers(username);
@@ -942,7 +944,7 @@ var init_sqlite = __esm({
942
944
  const result = stmt.run(now);
943
945
  return result.changes;
944
946
  }
945
- async answerOffer(offerId, answererUsername, answerSdp) {
947
+ async answerOffer(offerId, answererUsername, answerSdp, matchedTags) {
946
948
  const offer = await this.getOfferById(offerId);
947
949
  if (!offer) {
948
950
  return {
@@ -958,10 +960,11 @@ var init_sqlite = __esm({
958
960
  }
959
961
  const stmt = this.db.prepare(`
960
962
  UPDATE offers
961
- SET answerer_username = ?, answer_sdp = ?, answered_at = ?
963
+ SET answerer_username = ?, answer_sdp = ?, answered_at = ?, matched_tags = ?
962
964
  WHERE id = ? AND answerer_username IS NULL
963
965
  `);
964
- const result = stmt.run(answererUsername, answerSdp, Date.now(), offerId);
966
+ const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
967
+ const result = stmt.run(answererUsername, answerSdp, Date.now(), matchedTagsJson, offerId);
965
968
  if (result.changes === 0) {
966
969
  return {
967
970
  success: false,
@@ -1288,7 +1291,8 @@ var init_sqlite = __esm({
1288
1291
  lastSeen: row.last_seen,
1289
1292
  answererUsername: row.answerer_username || void 0,
1290
1293
  answerSdp: row.answer_sdp || void 0,
1291
- answeredAt: row.answered_at || void 0
1294
+ answeredAt: row.answered_at || void 0,
1295
+ matchedTags: row.matched_tags ? JSON.parse(row.matched_tags) : void 0
1292
1296
  };
1293
1297
  }
1294
1298
  };
@@ -1346,6 +1350,7 @@ var init_mysql = __esm({
1346
1350
  answerer_username VARCHAR(32),
1347
1351
  answer_sdp MEDIUMTEXT,
1348
1352
  answered_at BIGINT,
1353
+ matched_tags JSON,
1349
1354
  INDEX idx_offers_username (username),
1350
1355
  INDEX idx_offers_expires (expires_at),
1351
1356
  INDEX idx_offers_last_seen (last_seen),
@@ -1457,7 +1462,7 @@ var init_mysql = __esm({
1457
1462
  );
1458
1463
  return result.affectedRows;
1459
1464
  }
1460
- async answerOffer(offerId, answererUsername, answerSdp) {
1465
+ async answerOffer(offerId, answererUsername, answerSdp, matchedTags) {
1461
1466
  const offer = await this.getOfferById(offerId);
1462
1467
  if (!offer) {
1463
1468
  return { success: false, error: "Offer not found or expired" };
@@ -1465,10 +1470,11 @@ var init_mysql = __esm({
1465
1470
  if (offer.answererUsername) {
1466
1471
  return { success: false, error: "Offer already answered" };
1467
1472
  }
1473
+ const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
1468
1474
  const [result] = await this.pool.query(
1469
- `UPDATE offers SET answerer_username = ?, answer_sdp = ?, answered_at = ?
1475
+ `UPDATE offers SET answerer_username = ?, answer_sdp = ?, answered_at = ?, matched_tags = ?
1470
1476
  WHERE id = ? AND answerer_username IS NULL`,
1471
- [answererUsername, answerSdp, Date.now(), offerId]
1477
+ [answererUsername, answerSdp, Date.now(), matchedTagsJson, offerId]
1472
1478
  );
1473
1479
  if (result.affectedRows === 0) {
1474
1480
  return { success: false, error: "Offer already answered (race condition)" };
@@ -1758,7 +1764,8 @@ var init_mysql = __esm({
1758
1764
  lastSeen: Number(row.last_seen),
1759
1765
  answererUsername: row.answerer_username || void 0,
1760
1766
  answerSdp: row.answer_sdp || void 0,
1761
- answeredAt: row.answered_at ? Number(row.answered_at) : void 0
1767
+ answeredAt: row.answered_at ? Number(row.answered_at) : void 0,
1768
+ matchedTags: row.matched_tags ? typeof row.matched_tags === "string" ? JSON.parse(row.matched_tags) : row.matched_tags : void 0
1762
1769
  };
1763
1770
  }
1764
1771
  rowToIceCandidate(row) {
@@ -1823,7 +1830,8 @@ var init_postgres = __esm({
1823
1830
  last_seen BIGINT NOT NULL,
1824
1831
  answerer_username VARCHAR(32),
1825
1832
  answer_sdp TEXT,
1826
- answered_at BIGINT
1833
+ answered_at BIGINT,
1834
+ matched_tags JSONB
1827
1835
  )
1828
1836
  `);
1829
1837
  await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_username ON offers(username)`);
@@ -1935,7 +1943,7 @@ var init_postgres = __esm({
1935
1943
  );
1936
1944
  return result.rowCount ?? 0;
1937
1945
  }
1938
- async answerOffer(offerId, answererUsername, answerSdp) {
1946
+ async answerOffer(offerId, answererUsername, answerSdp, matchedTags) {
1939
1947
  const offer = await this.getOfferById(offerId);
1940
1948
  if (!offer) {
1941
1949
  return { success: false, error: "Offer not found or expired" };
@@ -1943,10 +1951,11 @@ var init_postgres = __esm({
1943
1951
  if (offer.answererUsername) {
1944
1952
  return { success: false, error: "Offer already answered" };
1945
1953
  }
1954
+ const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
1946
1955
  const result = await this.pool.query(
1947
- `UPDATE offers SET answerer_username = $1, answer_sdp = $2, answered_at = $3
1948
- WHERE id = $4 AND answerer_username IS NULL`,
1949
- [answererUsername, answerSdp, Date.now(), offerId]
1956
+ `UPDATE offers SET answerer_username = $1, answer_sdp = $2, answered_at = $3, matched_tags = $4
1957
+ WHERE id = $5 AND answerer_username IS NULL`,
1958
+ [answererUsername, answerSdp, Date.now(), matchedTagsJson, offerId]
1950
1959
  );
1951
1960
  if ((result.rowCount ?? 0) === 0) {
1952
1961
  return { success: false, error: "Offer already answered (race condition)" };
@@ -2244,7 +2253,8 @@ var init_postgres = __esm({
2244
2253
  lastSeen: Number(row.last_seen),
2245
2254
  answererUsername: row.answerer_username || void 0,
2246
2255
  answerSdp: row.answer_sdp || void 0,
2247
- answeredAt: row.answered_at ? Number(row.answered_at) : void 0
2256
+ answeredAt: row.answered_at ? Number(row.answered_at) : void 0,
2257
+ matchedTags: row.matched_tags || void 0
2248
2258
  };
2249
2259
  }
2250
2260
  rowToIceCandidate(row) {
@@ -2593,7 +2603,7 @@ var handlers = {
2593
2603
  * Answer an offer
2594
2604
  */
2595
2605
  async answerOffer(params, name, timestamp, signature, storage, config, request) {
2596
- const { offerId, sdp } = params;
2606
+ const { offerId, sdp, matchedTags } = params;
2597
2607
  validateStringParam(offerId, "offerId");
2598
2608
  if (!name) {
2599
2609
  throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Name required");
@@ -2604,6 +2614,9 @@ var handlers = {
2604
2614
  if (sdp.length > config.maxSdpSize) {
2605
2615
  throw new RpcError(ErrorCodes.SDP_TOO_LARGE, `SDP too large (max ${config.maxSdpSize} bytes)`);
2606
2616
  }
2617
+ if (matchedTags !== void 0 && !Array.isArray(matchedTags)) {
2618
+ throw new RpcError(ErrorCodes.INVALID_PARAMS, "matchedTags must be an array");
2619
+ }
2607
2620
  const offer = await storage.getOfferById(offerId);
2608
2621
  if (!offer) {
2609
2622
  throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, "Offer not found");
@@ -2611,7 +2624,14 @@ var handlers = {
2611
2624
  if (offer.answererUsername) {
2612
2625
  throw new RpcError(ErrorCodes.OFFER_ALREADY_ANSWERED, "Offer already answered");
2613
2626
  }
2614
- await storage.answerOffer(offerId, name, sdp);
2627
+ if (matchedTags && matchedTags.length > 0) {
2628
+ const offerTagSet = new Set(offer.tags);
2629
+ const invalidTags = matchedTags.filter((tag) => !offerTagSet.has(tag));
2630
+ if (invalidTags.length > 0) {
2631
+ throw new RpcError(ErrorCodes.INVALID_PARAMS, `matchedTags contains tags not on offer: ${invalidTags.join(", ")}`);
2632
+ }
2633
+ }
2634
+ await storage.answerOffer(offerId, name, sdp, matchedTags);
2615
2635
  return { success: true, offerId };
2616
2636
  },
2617
2637
  /**
@@ -3018,7 +3038,7 @@ function createApp(storage, config) {
3018
3038
  }
3019
3039
 
3020
3040
  // src/config.ts
3021
- var BUILD_VERSION = true ? "0.5.11" : "unknown";
3041
+ var BUILD_VERSION = true ? "0.5.12" : "unknown";
3022
3042
  function loadConfig() {
3023
3043
  let masterEncryptionKey = process.env.MASTER_ENCRYPTION_KEY;
3024
3044
  if (!masterEncryptionKey) {