@xtr-dev/rondevu-client 0.21.3 → 0.21.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/README.md +21 -25
- package/dist/api/client.d.ts +27 -40
- package/dist/api/client.js +39 -137
- package/dist/connections/answerer.d.ts +7 -5
- package/dist/connections/answerer.js +17 -16
- package/dist/connections/base.d.ts +2 -2
- package/dist/connections/offerer.d.ts +8 -8
- package/dist/connections/offerer.js +15 -15
- package/dist/core/offer-pool.d.ts +15 -4
- package/dist/core/offer-pool.js +30 -5
- package/dist/core/peer.d.ts +9 -9
- package/dist/core/peer.js +17 -16
- package/dist/core/polling-manager.d.ts +2 -1
- package/dist/core/polling-manager.js +2 -1
- package/dist/core/rondevu-types.d.ts +7 -9
- package/dist/core/rondevu.d.ts +26 -18
- package/dist/core/rondevu.js +44 -34
- package/dist/crypto/adapter.d.ts +19 -14
- package/dist/crypto/node.d.ts +14 -15
- package/dist/crypto/node.js +41 -53
- package/dist/crypto/web.d.ts +9 -12
- package/dist/crypto/web.js +36 -50
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ TypeScript client for [Rondevu](https://github.com/xtr-dev/rondevu-server), prov
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **
|
|
11
|
+
- **Ed25519 Identity**: Your public key IS your identity (like Ethereum addresses)
|
|
12
12
|
- **Tags-Based Discovery**: Find peers using tags (e.g., `["chat", "video"]`)
|
|
13
13
|
- **Automatic Reconnection**: Built-in exponential backoff
|
|
14
14
|
- **Message Buffering**: Queues messages during disconnections
|
|
@@ -27,10 +27,10 @@ import { Rondevu } from '@xtr-dev/rondevu-client'
|
|
|
27
27
|
// ============================================
|
|
28
28
|
// ALICE: Host and wait for connections
|
|
29
29
|
// ============================================
|
|
30
|
-
const alice = await Rondevu.connect(
|
|
30
|
+
const alice = await Rondevu.connect()
|
|
31
31
|
|
|
32
32
|
alice.on('connection:opened', (offerId, connection) => {
|
|
33
|
-
console.log('Connected to', connection.
|
|
33
|
+
console.log('Connected to', connection.peerPublicKey)
|
|
34
34
|
connection.on('message', (data) => console.log('Received:', data))
|
|
35
35
|
connection.send('Hello!')
|
|
36
36
|
})
|
|
@@ -43,10 +43,7 @@ const offer = await alice.offer({ tags: ['chat'], maxOffers: 5 })
|
|
|
43
43
|
// ============================================
|
|
44
44
|
const bob = await Rondevu.connect()
|
|
45
45
|
|
|
46
|
-
const peer = await bob.peer({
|
|
47
|
-
username: 'alice',
|
|
48
|
-
tags: ['chat']
|
|
49
|
-
})
|
|
46
|
+
const peer = await bob.peer({ tags: ['chat'] })
|
|
50
47
|
|
|
51
48
|
peer.on('open', () => peer.send('Hello Alice!'))
|
|
52
49
|
peer.on('message', (data) => console.log('Received:', data))
|
|
@@ -59,14 +56,13 @@ peer.on('message', (data) => console.log('Received:', data))
|
|
|
59
56
|
```typescript
|
|
60
57
|
const rondevu = await Rondevu.connect({
|
|
61
58
|
apiUrl?: string, // Default: 'https://api.ronde.vu'
|
|
62
|
-
|
|
63
|
-
username?: string, // Claim username (4-32 chars)
|
|
59
|
+
keyPair?: KeyPair, // Reuse existing keypair
|
|
64
60
|
iceServers?: IceServerPreset | RTCIceServer[], // Default: 'rondevu'
|
|
65
61
|
debug?: boolean
|
|
66
62
|
})
|
|
67
63
|
|
|
68
|
-
rondevu.
|
|
69
|
-
rondevu.
|
|
64
|
+
rondevu.getPublicKey() // Get public key (your identity)
|
|
65
|
+
rondevu.getKeyPair() // Get keypair for persistence
|
|
70
66
|
```
|
|
71
67
|
|
|
72
68
|
**ICE Presets**: `'rondevu'` (default), `'rondevu-relay'`, `'google-stun'`, `'public-stun'`
|
|
@@ -76,7 +72,7 @@ rondevu.getCredential() // Get credential for reuse
|
|
|
76
72
|
```typescript
|
|
77
73
|
const peer = await rondevu.peer({
|
|
78
74
|
tags: string[],
|
|
79
|
-
|
|
75
|
+
publicKey?: string, // Connect to specific peer
|
|
80
76
|
rtcConfig?: RTCConfiguration
|
|
81
77
|
})
|
|
82
78
|
|
|
@@ -89,7 +85,7 @@ peer.on('reconnecting', (attempt, max) => {})
|
|
|
89
85
|
|
|
90
86
|
// Properties & Methods
|
|
91
87
|
peer.state // 'connecting' | 'connected' | 'reconnecting' | ...
|
|
92
|
-
peer.
|
|
88
|
+
peer.peerPublicKey
|
|
93
89
|
peer.send(data)
|
|
94
90
|
peer.close()
|
|
95
91
|
```
|
|
@@ -116,25 +112,25 @@ rondevu.on('connection:opened', (offerId, connection) => {
|
|
|
116
112
|
|
|
117
113
|
```typescript
|
|
118
114
|
const result = await rondevu.discover(['chat'], { limit: 20 })
|
|
119
|
-
result.offers.forEach(o => console.log(o.
|
|
115
|
+
result.offers.forEach(o => console.log(o.publicKey, o.tags))
|
|
120
116
|
```
|
|
121
117
|
|
|
122
|
-
##
|
|
118
|
+
## Identity (Ed25519 Keypairs)
|
|
119
|
+
|
|
120
|
+
Your identity is an Ed25519 public key - no usernames, no registration, no claiming conflicts. Generate a keypair locally and start making requests immediately.
|
|
123
121
|
|
|
124
122
|
```typescript
|
|
125
|
-
// Auto-generated
|
|
123
|
+
// Auto-generated keypair
|
|
126
124
|
const rondevu = await Rondevu.connect()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Claimed username
|
|
130
|
-
const rondevu = await Rondevu.connect({ username: 'alice' })
|
|
125
|
+
console.log(rondevu.getPublicKey()) // '5a7f3e2d...' (64 hex chars)
|
|
131
126
|
|
|
132
|
-
// Save and restore
|
|
133
|
-
const
|
|
134
|
-
localStorage.setItem('
|
|
127
|
+
// Save and restore keypair for persistent identity
|
|
128
|
+
const keyPair = rondevu.getKeyPair()
|
|
129
|
+
localStorage.setItem('keypair', JSON.stringify(keyPair))
|
|
135
130
|
|
|
136
|
-
|
|
137
|
-
const
|
|
131
|
+
// Later: restore
|
|
132
|
+
const saved = JSON.parse(localStorage.getItem('keypair'))
|
|
133
|
+
const rondevu = await Rondevu.connect({ keyPair: saved })
|
|
138
134
|
```
|
|
139
135
|
|
|
140
136
|
## Tag Validation
|
package/dist/api/client.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rondevu API Client - RPC interface
|
|
3
3
|
*/
|
|
4
|
-
import { CryptoAdapter,
|
|
4
|
+
import { CryptoAdapter, KeyPair } from '../crypto/adapter.js';
|
|
5
5
|
import { BatcherOptions } from './batcher.js';
|
|
6
|
-
export type {
|
|
6
|
+
export type { KeyPair } from '../crypto/adapter.js';
|
|
7
7
|
export type { BatcherOptions } from './batcher.js';
|
|
8
8
|
export interface OfferRequest {
|
|
9
9
|
sdp: string;
|
|
@@ -20,7 +20,7 @@ export interface DiscoverRequest {
|
|
|
20
20
|
}
|
|
21
21
|
export interface TaggedOffer {
|
|
22
22
|
offerId: string;
|
|
23
|
-
|
|
23
|
+
publicKey: string;
|
|
24
24
|
tags: string[];
|
|
25
25
|
sdp: string;
|
|
26
26
|
createdAt: number;
|
|
@@ -33,7 +33,7 @@ export interface DiscoverResponse {
|
|
|
33
33
|
offset: number;
|
|
34
34
|
}
|
|
35
35
|
export interface PublishResponse {
|
|
36
|
-
|
|
36
|
+
publicKey: string;
|
|
37
37
|
tags: string[];
|
|
38
38
|
offers: Array<{
|
|
39
39
|
offerId: string;
|
|
@@ -51,19 +51,19 @@ export interface IceCandidate {
|
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* RondevuAPI - RPC-based API client for Rondevu signaling server
|
|
54
|
+
*
|
|
55
|
+
* Uses Ed25519 public key cryptography for authentication.
|
|
56
|
+
* The public key IS the identity (like Ethereum addresses).
|
|
54
57
|
*/
|
|
55
58
|
export declare class RondevuAPI {
|
|
56
59
|
private baseUrl;
|
|
57
|
-
private
|
|
58
|
-
private static readonly
|
|
59
|
-
private static readonly
|
|
60
|
-
private static readonly DEFAULT_CREDENTIAL_NAME_MAX_LENGTH;
|
|
61
|
-
private static readonly DEFAULT_SECRET_MIN_LENGTH;
|
|
62
|
-
private static readonly MAX_BACKOFF_MS;
|
|
60
|
+
private keyPair;
|
|
61
|
+
private static readonly PUBLIC_KEY_LENGTH;
|
|
62
|
+
private static readonly PRIVATE_KEY_LENGTH;
|
|
63
63
|
private static readonly MAX_CANONICALIZE_DEPTH;
|
|
64
64
|
private crypto;
|
|
65
65
|
private batcher;
|
|
66
|
-
constructor(baseUrl: string,
|
|
66
|
+
constructor(baseUrl: string, keyPair: KeyPair, cryptoAdapter?: CryptoAdapter, batcherOptions?: BatcherOptions);
|
|
67
67
|
/**
|
|
68
68
|
* Canonical JSON serialization with sorted keys
|
|
69
69
|
* Ensures deterministic output regardless of property insertion order
|
|
@@ -90,7 +90,7 @@ export declare class RondevuAPI {
|
|
|
90
90
|
private generateNonce;
|
|
91
91
|
/**
|
|
92
92
|
* Generate authentication headers for RPC request
|
|
93
|
-
* Uses
|
|
93
|
+
* Uses Ed25519 signature with nonce for replay protection
|
|
94
94
|
*
|
|
95
95
|
* Security notes:
|
|
96
96
|
* - Nonce: Cryptographically secure random value (UUID or 128-bit hex)
|
|
@@ -98,7 +98,7 @@ export declare class RondevuAPI {
|
|
|
98
98
|
* - Server validates timestamp is within acceptable range (typically ±5 minutes)
|
|
99
99
|
* - Tolerates reasonable clock skew between client and server
|
|
100
100
|
* - Requests with stale timestamps are rejected
|
|
101
|
-
* - Signature:
|
|
101
|
+
* - Signature: Ed25519 ensures message integrity and authenticity
|
|
102
102
|
* - Server validates nonce uniqueness to prevent replay within time window
|
|
103
103
|
* - Each nonce can only be used once within the timestamp validity window
|
|
104
104
|
* - Server maintains nonce cache with expiration matching timestamp window
|
|
@@ -110,31 +110,13 @@ export declare class RondevuAPI {
|
|
|
110
110
|
*/
|
|
111
111
|
private rpc;
|
|
112
112
|
/**
|
|
113
|
-
* Generate new
|
|
114
|
-
* This is
|
|
115
|
-
* Credentials are generated server-side to ensure security and uniqueness
|
|
113
|
+
* Generate a new Ed25519 keypair locally
|
|
114
|
+
* This is completely client-side - no server communication
|
|
116
115
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* - The secret provides full access to this identity
|
|
120
|
-
* - Credentials should be persisted encrypted and never logged
|
|
121
|
-
*
|
|
122
|
-
* @param baseUrl - Rondevu server URL
|
|
123
|
-
* @param expiresAt - Optional custom expiry timestamp (defaults to 1 year)
|
|
124
|
-
* @param options - Optional: { maxRetries: number, timeout: number }
|
|
125
|
-
* @returns Generated credential with name and secret
|
|
126
|
-
*/
|
|
127
|
-
static generateCredentials(baseUrl: string, options?: {
|
|
128
|
-
name?: string;
|
|
129
|
-
expiresAt?: number;
|
|
130
|
-
maxRetries?: number;
|
|
131
|
-
timeout?: number;
|
|
132
|
-
}): Promise<Credential>;
|
|
133
|
-
/**
|
|
134
|
-
* Generate a random secret locally (for advanced use cases)
|
|
135
|
-
* @param cryptoAdapter - Optional crypto adapter
|
|
116
|
+
* @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
|
|
117
|
+
* @returns Generated keypair with publicKey and privateKey as hex strings
|
|
136
118
|
*/
|
|
137
|
-
static
|
|
119
|
+
static generateKeyPair(cryptoAdapter?: CryptoAdapter): Promise<KeyPair>;
|
|
138
120
|
/**
|
|
139
121
|
* Publish offers with tags
|
|
140
122
|
*/
|
|
@@ -153,16 +135,20 @@ export declare class RondevuAPI {
|
|
|
153
135
|
}>;
|
|
154
136
|
/**
|
|
155
137
|
* Answer an offer
|
|
138
|
+
* @param offerId The offer ID to answer
|
|
139
|
+
* @param sdp The SDP answer
|
|
140
|
+
* @param matchedTags Optional tags that were used to discover this offer
|
|
156
141
|
*/
|
|
157
|
-
answerOffer(offerId: string, sdp: string): Promise<void>;
|
|
142
|
+
answerOffer(offerId: string, sdp: string, matchedTags?: string[]): Promise<void>;
|
|
158
143
|
/**
|
|
159
144
|
* Get answer for a specific offer (offerer polls this)
|
|
160
145
|
*/
|
|
161
146
|
getOfferAnswer(offerId: string): Promise<{
|
|
162
147
|
sdp: string;
|
|
163
148
|
offerId: string;
|
|
164
|
-
|
|
149
|
+
answererPublicKey: string;
|
|
165
150
|
answeredAt: number;
|
|
151
|
+
matchedTags?: string[];
|
|
166
152
|
} | null>;
|
|
167
153
|
/**
|
|
168
154
|
* Combined polling for answers and ICE candidates
|
|
@@ -170,14 +156,15 @@ export declare class RondevuAPI {
|
|
|
170
156
|
poll(since?: number): Promise<{
|
|
171
157
|
answers: Array<{
|
|
172
158
|
offerId: string;
|
|
173
|
-
|
|
159
|
+
answererPublicKey: string;
|
|
174
160
|
sdp: string;
|
|
175
161
|
answeredAt: number;
|
|
162
|
+
matchedTags?: string[];
|
|
176
163
|
}>;
|
|
177
164
|
iceCandidates: Record<string, Array<{
|
|
178
165
|
candidate: RTCIceCandidateInit | null;
|
|
179
166
|
role: 'offerer' | 'answerer';
|
|
180
|
-
|
|
167
|
+
peerPublicKey: string;
|
|
181
168
|
createdAt: number;
|
|
182
169
|
}>>;
|
|
183
170
|
}>;
|
package/dist/api/client.js
CHANGED
|
@@ -5,41 +5,37 @@ import { WebCryptoAdapter } from '../crypto/web.js';
|
|
|
5
5
|
import { RpcBatcher } from './batcher.js';
|
|
6
6
|
/**
|
|
7
7
|
* RondevuAPI - RPC-based API client for Rondevu signaling server
|
|
8
|
+
*
|
|
9
|
+
* Uses Ed25519 public key cryptography for authentication.
|
|
10
|
+
* The public key IS the identity (like Ethereum addresses).
|
|
8
11
|
*/
|
|
9
12
|
export class RondevuAPI {
|
|
10
|
-
constructor(baseUrl,
|
|
13
|
+
constructor(baseUrl, keyPair, cryptoAdapter, batcherOptions) {
|
|
11
14
|
this.baseUrl = baseUrl;
|
|
12
|
-
this.
|
|
15
|
+
this.keyPair = keyPair;
|
|
13
16
|
// Use WebCryptoAdapter by default (browser environment)
|
|
14
17
|
this.crypto = cryptoAdapter || new WebCryptoAdapter();
|
|
15
18
|
// Create batcher for request batching with throttling
|
|
16
19
|
this.batcher = new RpcBatcher(baseUrl, batcherOptions);
|
|
17
|
-
// Validate
|
|
18
|
-
if (!
|
|
19
|
-
throw new Error('Invalid
|
|
20
|
+
// Validate public key format
|
|
21
|
+
if (!keyPair.publicKey || typeof keyPair.publicKey !== 'string') {
|
|
22
|
+
throw new Error('Invalid keypair: publicKey must be a non-empty string');
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (credential.name.length > RondevuAPI.DEFAULT_CREDENTIAL_NAME_MAX_LENGTH) {
|
|
24
|
-
throw new Error(`Invalid credential: name must not exceed ${RondevuAPI.DEFAULT_CREDENTIAL_NAME_MAX_LENGTH} characters`);
|
|
24
|
+
if (keyPair.publicKey.length !== RondevuAPI.PUBLIC_KEY_LENGTH) {
|
|
25
|
+
throw new Error(`Invalid keypair: publicKey must be ${RondevuAPI.PUBLIC_KEY_LENGTH} hex characters (32 bytes)`);
|
|
25
26
|
}
|
|
26
|
-
if (!/^[
|
|
27
|
-
throw new Error('Invalid
|
|
27
|
+
if (!/^[0-9a-fA-F]+$/.test(keyPair.publicKey)) {
|
|
28
|
+
throw new Error('Invalid keypair: publicKey must contain only hexadecimal characters');
|
|
28
29
|
}
|
|
29
|
-
// Validate
|
|
30
|
-
if (!
|
|
31
|
-
throw new Error('Invalid
|
|
30
|
+
// Validate private key format
|
|
31
|
+
if (!keyPair.privateKey || typeof keyPair.privateKey !== 'string') {
|
|
32
|
+
throw new Error('Invalid keypair: privateKey must be a non-empty string');
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error(`Invalid credential: secret must be at least 256 bits (${RondevuAPI.DEFAULT_SECRET_MIN_LENGTH} hex characters)`);
|
|
34
|
+
if (keyPair.privateKey.length !== RondevuAPI.PRIVATE_KEY_LENGTH) {
|
|
35
|
+
throw new Error(`Invalid keypair: privateKey must be ${RondevuAPI.PRIVATE_KEY_LENGTH} hex characters (32 bytes)`);
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new Error('Invalid credential: secret must be a valid hex string (even length)');
|
|
40
|
-
}
|
|
41
|
-
if (!/^[0-9a-fA-F]+$/.test(credential.secret)) {
|
|
42
|
-
throw new Error('Invalid credential: secret must contain only hexadecimal characters');
|
|
37
|
+
if (!/^[0-9a-fA-F]+$/.test(keyPair.privateKey)) {
|
|
38
|
+
throw new Error('Invalid keypair: privateKey must contain only hexadecimal characters');
|
|
43
39
|
}
|
|
44
40
|
}
|
|
45
41
|
/**
|
|
@@ -133,7 +129,7 @@ export class RondevuAPI {
|
|
|
133
129
|
}
|
|
134
130
|
/**
|
|
135
131
|
* Generate authentication headers for RPC request
|
|
136
|
-
* Uses
|
|
132
|
+
* Uses Ed25519 signature with nonce for replay protection
|
|
137
133
|
*
|
|
138
134
|
* Security notes:
|
|
139
135
|
* - Nonce: Cryptographically secure random value (UUID or 128-bit hex)
|
|
@@ -141,7 +137,7 @@ export class RondevuAPI {
|
|
|
141
137
|
* - Server validates timestamp is within acceptable range (typically ±5 minutes)
|
|
142
138
|
* - Tolerates reasonable clock skew between client and server
|
|
143
139
|
* - Requests with stale timestamps are rejected
|
|
144
|
-
* - Signature:
|
|
140
|
+
* - Signature: Ed25519 ensures message integrity and authenticity
|
|
145
141
|
* - Server validates nonce uniqueness to prevent replay within time window
|
|
146
142
|
* - Each nonce can only be used once within the timestamp validity window
|
|
147
143
|
* - Server maintains nonce cache with expiration matching timestamp window
|
|
@@ -149,11 +145,11 @@ export class RondevuAPI {
|
|
|
149
145
|
async generateAuthHeaders(request) {
|
|
150
146
|
const timestamp = Date.now();
|
|
151
147
|
const nonce = this.generateNonce();
|
|
152
|
-
// Build message and generate signature
|
|
148
|
+
// Build message and generate Ed25519 signature
|
|
153
149
|
const message = this.buildSignatureMessage(timestamp, nonce, request.method, request.params);
|
|
154
|
-
const signature = await this.crypto.
|
|
150
|
+
const signature = await this.crypto.signMessage(this.keyPair.privateKey, message);
|
|
155
151
|
return {
|
|
156
|
-
'X-
|
|
152
|
+
'X-PublicKey': this.keyPair.publicKey,
|
|
157
153
|
'X-Timestamp': timestamp.toString(),
|
|
158
154
|
'X-Nonce': nonce,
|
|
159
155
|
'X-Signature': signature,
|
|
@@ -167,112 +163,18 @@ export class RondevuAPI {
|
|
|
167
163
|
return this.batcher.add(request, authHeaders);
|
|
168
164
|
}
|
|
169
165
|
// ============================================
|
|
170
|
-
//
|
|
166
|
+
// Identity Management (Ed25519 Public Key)
|
|
171
167
|
// ============================================
|
|
172
168
|
/**
|
|
173
|
-
* Generate new
|
|
174
|
-
* This is
|
|
175
|
-
* Credentials are generated server-side to ensure security and uniqueness
|
|
176
|
-
*
|
|
177
|
-
* ⚠️ SECURITY NOTE:
|
|
178
|
-
* - Store the returned credential securely
|
|
179
|
-
* - The secret provides full access to this identity
|
|
180
|
-
* - Credentials should be persisted encrypted and never logged
|
|
169
|
+
* Generate a new Ed25519 keypair locally
|
|
170
|
+
* This is completely client-side - no server communication
|
|
181
171
|
*
|
|
182
|
-
* @param
|
|
183
|
-
* @
|
|
184
|
-
* @param options - Optional: { maxRetries: number, timeout: number }
|
|
185
|
-
* @returns Generated credential with name and secret
|
|
186
|
-
*/
|
|
187
|
-
static async generateCredentials(baseUrl, options) {
|
|
188
|
-
const maxRetries = options?.maxRetries ?? RondevuAPI.DEFAULT_MAX_RETRIES;
|
|
189
|
-
const timeout = options?.timeout ?? RondevuAPI.DEFAULT_TIMEOUT_MS;
|
|
190
|
-
let lastError = null;
|
|
191
|
-
// Build params object with optional name and expiresAt
|
|
192
|
-
const params = {};
|
|
193
|
-
if (options?.name)
|
|
194
|
-
params.name = options.name;
|
|
195
|
-
if (options?.expiresAt)
|
|
196
|
-
params.expiresAt = options.expiresAt;
|
|
197
|
-
const request = {
|
|
198
|
-
method: 'generateCredentials',
|
|
199
|
-
params: Object.keys(params).length > 0 ? params : undefined,
|
|
200
|
-
};
|
|
201
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
202
|
-
// httpStatus is scoped to each iteration intentionally - resets on each retry
|
|
203
|
-
let httpStatus = null;
|
|
204
|
-
try {
|
|
205
|
-
// Create abort controller for timeout
|
|
206
|
-
if (typeof AbortController === 'undefined') {
|
|
207
|
-
throw new Error('AbortController not supported in this environment');
|
|
208
|
-
}
|
|
209
|
-
const controller = new AbortController();
|
|
210
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
211
|
-
try {
|
|
212
|
-
const response = await fetch(`${baseUrl}/rpc`, {
|
|
213
|
-
method: 'POST',
|
|
214
|
-
headers: {
|
|
215
|
-
'Content-Type': 'application/json',
|
|
216
|
-
},
|
|
217
|
-
body: JSON.stringify([request]), // Server expects array (batch format)
|
|
218
|
-
signal: controller.signal,
|
|
219
|
-
});
|
|
220
|
-
httpStatus = response.status;
|
|
221
|
-
if (!response.ok) {
|
|
222
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
223
|
-
}
|
|
224
|
-
// Server returns array of responses
|
|
225
|
-
const results = await response.json();
|
|
226
|
-
const result = results[0];
|
|
227
|
-
if (!result || !result.success) {
|
|
228
|
-
throw new Error(result?.error || 'Failed to generate credentials');
|
|
229
|
-
}
|
|
230
|
-
// Validate credential structure
|
|
231
|
-
const credential = result.result;
|
|
232
|
-
if (!credential || typeof credential !== 'object') {
|
|
233
|
-
throw new Error('Invalid credential response: result is not an object');
|
|
234
|
-
}
|
|
235
|
-
if (typeof credential.name !== 'string' || !credential.name) {
|
|
236
|
-
throw new Error('Invalid credential response: missing or invalid name');
|
|
237
|
-
}
|
|
238
|
-
if (typeof credential.secret !== 'string' || !credential.secret) {
|
|
239
|
-
throw new Error('Invalid credential response: missing or invalid secret');
|
|
240
|
-
}
|
|
241
|
-
return credential;
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
// Always clear timeout to prevent memory leaks
|
|
245
|
-
clearTimeout(timeoutId);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
lastError = error;
|
|
250
|
-
// Don't retry on abort (timeout)
|
|
251
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
252
|
-
throw new Error(`Credential generation timed out after ${timeout}ms`);
|
|
253
|
-
}
|
|
254
|
-
// Don't retry on 4xx errors (client errors) - check actual status
|
|
255
|
-
if (httpStatus !== null && httpStatus >= 400 && httpStatus < 500) {
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
// Retry with exponential backoff + jitter for network/server errors (5xx or network failures)
|
|
259
|
-
// Jitter prevents thundering herd when many clients retry simultaneously
|
|
260
|
-
// Cap backoff to prevent excessive waits
|
|
261
|
-
if (attempt < maxRetries - 1) {
|
|
262
|
-
const backoffMs = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, RondevuAPI.MAX_BACKOFF_MS);
|
|
263
|
-
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
throw new Error(`Failed to generate credentials after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Generate a random secret locally (for advanced use cases)
|
|
271
|
-
* @param cryptoAdapter - Optional crypto adapter
|
|
172
|
+
* @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
|
|
173
|
+
* @returns Generated keypair with publicKey and privateKey as hex strings
|
|
272
174
|
*/
|
|
273
|
-
static
|
|
175
|
+
static async generateKeyPair(cryptoAdapter) {
|
|
274
176
|
const adapter = cryptoAdapter || new WebCryptoAdapter();
|
|
275
|
-
return adapter.
|
|
177
|
+
return adapter.generateKeyPair();
|
|
276
178
|
}
|
|
277
179
|
// ============================================
|
|
278
180
|
// Tags-based Offer Management (v2)
|
|
@@ -325,11 +227,14 @@ export class RondevuAPI {
|
|
|
325
227
|
// ============================================
|
|
326
228
|
/**
|
|
327
229
|
* Answer an offer
|
|
230
|
+
* @param offerId The offer ID to answer
|
|
231
|
+
* @param sdp The SDP answer
|
|
232
|
+
* @param matchedTags Optional tags that were used to discover this offer
|
|
328
233
|
*/
|
|
329
|
-
async answerOffer(offerId, sdp) {
|
|
234
|
+
async answerOffer(offerId, sdp, matchedTags) {
|
|
330
235
|
const request = {
|
|
331
236
|
method: 'answerOffer',
|
|
332
|
-
params: { offerId, sdp },
|
|
237
|
+
params: { offerId, sdp, matchedTags },
|
|
333
238
|
};
|
|
334
239
|
const authHeaders = await this.generateAuthHeaders(request);
|
|
335
240
|
await this.rpc(request, authHeaders);
|
|
@@ -391,10 +296,7 @@ export class RondevuAPI {
|
|
|
391
296
|
};
|
|
392
297
|
}
|
|
393
298
|
}
|
|
394
|
-
//
|
|
395
|
-
RondevuAPI.
|
|
396
|
-
RondevuAPI.
|
|
397
|
-
RondevuAPI.DEFAULT_CREDENTIAL_NAME_MAX_LENGTH = 128;
|
|
398
|
-
RondevuAPI.DEFAULT_SECRET_MIN_LENGTH = 64; // 256 bits
|
|
399
|
-
RondevuAPI.MAX_BACKOFF_MS = 60000; // 60 seconds max backoff
|
|
299
|
+
// Key length constants
|
|
300
|
+
RondevuAPI.PUBLIC_KEY_LENGTH = 64; // 32 bytes = 64 hex chars
|
|
301
|
+
RondevuAPI.PRIVATE_KEY_LENGTH = 64; // 32 bytes = 64 hex chars
|
|
400
302
|
RondevuAPI.MAX_CANONICALIZE_DEPTH = 100; // Prevent stack overflow
|
|
@@ -7,23 +7,25 @@ import { ConnectionConfig } from './config.js';
|
|
|
7
7
|
import { WebRTCAdapter } from '../webrtc/adapter.js';
|
|
8
8
|
export interface AnswererOptions {
|
|
9
9
|
api: RondevuAPI;
|
|
10
|
-
|
|
10
|
+
ownerPublicKey: string;
|
|
11
11
|
tags: string[];
|
|
12
12
|
offerId: string;
|
|
13
13
|
offerSdp: string;
|
|
14
14
|
rtcConfig?: RTCConfiguration;
|
|
15
15
|
webrtcAdapter?: WebRTCAdapter;
|
|
16
16
|
config?: Partial<ConnectionConfig>;
|
|
17
|
+
matchedTags?: string[];
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* Answerer connection - processes offers and creates answers
|
|
20
21
|
*/
|
|
21
22
|
export declare class AnswererConnection extends RondevuConnection {
|
|
22
23
|
private api;
|
|
23
|
-
private
|
|
24
|
+
private ownerPublicKey;
|
|
24
25
|
private tags;
|
|
25
26
|
private offerId;
|
|
26
27
|
private offerSdp;
|
|
28
|
+
private matchedTags?;
|
|
27
29
|
constructor(options: AnswererOptions);
|
|
28
30
|
/**
|
|
29
31
|
* Initialize the connection by processing offer and creating answer
|
|
@@ -38,15 +40,15 @@ export declare class AnswererConnection extends RondevuConnection {
|
|
|
38
40
|
*/
|
|
39
41
|
protected getApi(): any;
|
|
40
42
|
/**
|
|
41
|
-
* Get the owner
|
|
43
|
+
* Get the owner public key (implements abstract method)
|
|
42
44
|
*/
|
|
43
|
-
protected
|
|
45
|
+
protected getOwnerPublicKey(): string;
|
|
44
46
|
/**
|
|
45
47
|
* Answerers accept ICE candidates from offerers only
|
|
46
48
|
*/
|
|
47
49
|
protected getIceCandidateRole(): 'offerer' | null;
|
|
48
50
|
/**
|
|
49
|
-
* Attempt to reconnect to the same
|
|
51
|
+
* Attempt to reconnect to the same peer
|
|
50
52
|
*/
|
|
51
53
|
protected attemptReconnect(): void;
|
|
52
54
|
/**
|
|
@@ -10,10 +10,11 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
10
10
|
constructor(options) {
|
|
11
11
|
super(options.rtcConfig, options.config, options.webrtcAdapter);
|
|
12
12
|
this.api = options.api;
|
|
13
|
-
this.
|
|
13
|
+
this.ownerPublicKey = options.ownerPublicKey;
|
|
14
14
|
this.tags = options.tags;
|
|
15
15
|
this.offerId = options.offerId;
|
|
16
16
|
this.offerSdp = options.offerSdp;
|
|
17
|
+
this.matchedTags = options.matchedTags;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* Initialize the connection by processing offer and creating answer
|
|
@@ -43,8 +44,8 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
43
44
|
const answer = await this.pc.createAnswer();
|
|
44
45
|
await this.pc.setLocalDescription(answer);
|
|
45
46
|
this.debug('Answer created, sending to server');
|
|
46
|
-
// Send answer to server
|
|
47
|
-
await this.api.answerOffer(this.offerId, answer.sdp);
|
|
47
|
+
// Send answer to server (including matched tags so offerer knows which tags we searched for)
|
|
48
|
+
await this.api.answerOffer(this.offerId, answer.sdp, this.matchedTags);
|
|
48
49
|
// Note: ICE candidate polling is handled by PollingManager
|
|
49
50
|
// Candidates are received via handleRemoteIceCandidates()
|
|
50
51
|
this.debug('Answer sent successfully');
|
|
@@ -75,10 +76,10 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
75
76
|
return this.api;
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
78
|
-
* Get the owner
|
|
79
|
+
* Get the owner public key (implements abstract method)
|
|
79
80
|
*/
|
|
80
|
-
|
|
81
|
-
return this.
|
|
81
|
+
getOwnerPublicKey() {
|
|
82
|
+
return this.ownerPublicKey;
|
|
82
83
|
}
|
|
83
84
|
/**
|
|
84
85
|
* Answerers accept ICE candidates from offerers only
|
|
@@ -87,11 +88,11 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
87
88
|
return 'offerer';
|
|
88
89
|
}
|
|
89
90
|
/**
|
|
90
|
-
* Attempt to reconnect to the same
|
|
91
|
+
* Attempt to reconnect to the same peer
|
|
91
92
|
*/
|
|
92
93
|
attemptReconnect() {
|
|
93
|
-
this.debug(`Attempting to reconnect to ${this.
|
|
94
|
-
// For answerer, we need to fetch a new offer from the same
|
|
94
|
+
this.debug(`Attempting to reconnect to ${this.ownerPublicKey}`);
|
|
95
|
+
// For answerer, we need to fetch a new offer from the same peer
|
|
95
96
|
// Clean up old connection
|
|
96
97
|
if (this.pc) {
|
|
97
98
|
this.pc.close();
|
|
@@ -109,16 +110,16 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
109
110
|
if (!response || !response.offers || response.offers.length === 0) {
|
|
110
111
|
throw new Error('No offers available for reconnection');
|
|
111
112
|
}
|
|
112
|
-
// Filter for offers from the same
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
throw new Error(`No offers available from ${this.
|
|
113
|
+
// Filter for offers from the same peer
|
|
114
|
+
const peerOffers = response.offers.filter(o => o.publicKey === this.ownerPublicKey);
|
|
115
|
+
if (peerOffers.length === 0) {
|
|
116
|
+
throw new Error(`No offers available from ${this.ownerPublicKey}`);
|
|
116
117
|
}
|
|
117
|
-
// Pick a random offer from the same
|
|
118
|
-
const offer =
|
|
118
|
+
// Pick a random offer from the same peer
|
|
119
|
+
const offer = peerOffers[Math.floor(Math.random() * peerOffers.length)];
|
|
119
120
|
this.offerId = offer.offerId;
|
|
120
121
|
this.offerSdp = offer.sdp;
|
|
121
|
-
this.debug(`Found new offer ${offer.offerId} from ${this.
|
|
122
|
+
this.debug(`Found new offer ${offer.offerId} from ${this.ownerPublicKey}`);
|
|
122
123
|
// Reinitialize with new offer
|
|
123
124
|
return this.initialize();
|
|
124
125
|
})
|
|
@@ -89,9 +89,9 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
89
89
|
*/
|
|
90
90
|
protected abstract getApi(): any;
|
|
91
91
|
/**
|
|
92
|
-
* Get the owner
|
|
92
|
+
* Get the owner public key - subclasses must provide
|
|
93
93
|
*/
|
|
94
|
-
protected abstract
|
|
94
|
+
protected abstract getOwnerPublicKey(): string;
|
|
95
95
|
/**
|
|
96
96
|
* Get the offer ID - subclasses must provide
|
|
97
97
|
*/
|