4 Commits

Author SHA1 Message Date
08e1433088 Update README: Remove custom peer ID documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:23:09 +01:00
70d018c666 Remove custom peer ID feature for security
Always generate cryptographically random 128-bit peer IDs to prevent peer ID hijacking vulnerability. This ensures peer IDs are secure through collision resistance rather than relying on expiration-based protection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:19:16 +01:00
2cff4c8544 0.1.4 2025-11-22 17:32:56 +01:00
00499732c4 Add optional info field to offers
- Add info field to Offer and CreateOfferRequest types
- Validate info field: optional, max 128 characters
- Include info field in all public API responses
- Update README with info field documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 17:32:56 +01:00
5 changed files with 31 additions and 45 deletions

View File

@@ -53,16 +53,7 @@ Health check endpoint with version
#### `POST /register`
Register a new peer and receive credentials (peerId + secret)
**Request (optional):**
```json
{
"peerId": "my-custom-peer-id"
}
```
**Notes:**
- `peerId` (optional): Custom peer ID (1-128 characters). If not provided, a random ID will be generated.
- Returns 409 Conflict if the custom peer ID is already in use.
Generates a cryptographically random 128-bit peer ID.
**Response:**
```json
@@ -111,7 +102,8 @@ Find offers by topic with optional bloom filter exclusion
"topics": ["movie-xyz", "hd-content"],
"expiresAt": 1234567890,
"lastSeen": 1234567890,
"hasSecret": true // Indicates if secret is required to answer
"hasSecret": true, // Indicates if secret is required to answer
"info": "Looking for peers in EU region" // Public info field (optional)
}
],
"total": 42,
@@ -121,6 +113,7 @@ Find offers by topic with optional bloom filter exclusion
**Notes:**
- `hasSecret`: Boolean flag indicating whether a secret is required to answer this offer. The actual secret is never exposed in public endpoints.
- `info`: Optional public metadata field (max 128 characters) visible to all peers.
#### `GET /peers/:peerId/offers`
View all offers from a specific peer
@@ -140,7 +133,8 @@ Create one or more offers
"sdp": "v=0...",
"topics": ["movie-xyz", "hd-content"],
"ttl": 300000,
"secret": "my-secret-password" // Optional: protect offer (max 128 chars)
"secret": "my-secret-password", // Optional: protect offer (max 128 chars)
"info": "Looking for peers in EU region" // Optional: public info (max 128 chars)
}
]
}
@@ -148,6 +142,7 @@ Create one or more offers
**Notes:**
- `secret` (optional): Protect the offer with a secret. Answerers must provide the correct secret to connect.
- `info` (optional): Public metadata visible to all peers (max 128 characters). Useful for describing the offer or connection requirements.
#### `GET /offers/mine`
List all offers owned by authenticated peer

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@xtr-dev/rondevu-server",
"version": "0.1.3",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xtr-dev/rondevu-server",
"version": "0.1.3",
"version": "0.1.4",
"dependencies": {
"@hono/node-server": "^1.19.6",
"better-sqlite3": "^12.4.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@xtr-dev/rondevu-server",
"version": "0.1.3",
"version": "0.1.5",
"description": "Topic-based peer discovery and signaling server for distributed P2P applications",
"main": "dist/index.js",
"scripts": {

View File

@@ -64,37 +64,12 @@ export function createApp(storage: Storage, config: Config) {
/**
* POST /register
* Register a new peer and receive credentials
* Accepts optional peerId in request body for custom peer IDs
* Generates a cryptographically random peer ID (128-bit)
*/
app.post('/register', async (c) => {
try {
let peerId: string;
// Check if custom peer ID is provided
const body = await c.req.json().catch(() => ({}));
const customPeerId = body.peerId;
if (customPeerId !== undefined) {
// Validate custom peer ID
if (typeof customPeerId !== 'string' || customPeerId.length === 0) {
return c.json({ error: 'Peer ID must be a non-empty string' }, 400);
}
if (customPeerId.length > 128) {
return c.json({ error: 'Peer ID must be 128 characters or less' }, 400);
}
// Check if peer ID is already in use by checking for active offers
const existingOffers = await storage.getOffersByPeerId(customPeerId);
if (existingOffers.length > 0) {
return c.json({ error: 'Peer ID is already in use' }, 409);
}
peerId = customPeerId;
} else {
// Generate new peer ID
peerId = generatePeerId();
}
// Always generate a random peer ID
const peerId = generatePeerId();
// Encrypt peer ID with server secret (async operation)
const secret = await encryptPeerId(peerId, config.authSecret);
@@ -151,6 +126,16 @@ export function createApp(storage: Storage, config: Config) {
}
}
// Validate info if provided
if (offer.info !== undefined) {
if (typeof offer.info !== 'string') {
return c.json({ error: 'Info must be a string' }, 400);
}
if (offer.info.length > 128) {
return c.json({ error: 'Info must be 128 characters or less' }, 400);
}
}
// Validate topics
if (!Array.isArray(offer.topics) || offer.topics.length === 0) {
return c.json({ error: 'Each offer must have a non-empty topics array' }, 400);
@@ -182,6 +167,7 @@ export function createApp(storage: Storage, config: Config) {
topics: offer.topics,
expiresAt: Date.now() + ttl,
secret: offer.secret,
info: offer.info,
});
}
@@ -254,7 +240,8 @@ export function createApp(storage: Storage, config: Config) {
topics: o.topics,
expiresAt: o.expiresAt,
lastSeen: o.lastSeen,
hasSecret: !!o.secret // Indicate if secret is required without exposing it
hasSecret: !!o.secret, // Indicate if secret is required without exposing it
info: o.info // Public info field
})),
total: bloomParam ? total + excludePeerIds.length : total,
returned: offers.length
@@ -321,7 +308,8 @@ export function createApp(storage: Storage, config: Config) {
topics: o.topics,
expiresAt: o.expiresAt,
lastSeen: o.lastSeen,
hasSecret: !!o.secret // Indicate if secret is required without exposing it
hasSecret: !!o.secret, // Indicate if secret is required without exposing it
info: o.info // Public info field
})),
topics: Array.from(topicsSet)
}, 200);
@@ -351,6 +339,7 @@ export function createApp(storage: Storage, config: Config) {
expiresAt: o.expiresAt,
lastSeen: o.lastSeen,
secret: o.secret, // Owner can see the secret
info: o.info, // Owner can see the info
answererPeerId: o.answererPeerId,
answeredAt: o.answeredAt
}))

View File

@@ -10,6 +10,7 @@ export interface Offer {
expiresAt: number;
lastSeen: number;
secret?: string;
info?: string;
answererPeerId?: string;
answerSdp?: string;
answeredAt?: number;
@@ -46,6 +47,7 @@ export interface CreateOfferRequest {
topics: string[];
expiresAt: number;
secret?: string;
info?: string;
}
/**