@xtr-dev/rondevu-client 0.21.5 → 0.21.8
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 +21 -39
- package/dist/api/client.js +34 -135
- package/dist/connections/answerer.d.ts +5 -5
- package/dist/connections/answerer.js +14 -14
- 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/index.d.ts +1 -0
- package/dist/core/offer-pool.d.ts +9 -2
- package/dist/core/offer-pool.js +8 -3
- package/dist/core/peer.d.ts +9 -9
- package/dist/core/peer.js +16 -16
- package/dist/core/polling-manager.d.ts +1 -1
- package/dist/core/polling-manager.js +1 -1
- package/dist/core/rondevu-types.d.ts +9 -9
- package/dist/core/rondevu.d.ts +25 -18
- package/dist/core/rondevu.js +46 -33
- 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/dist/meta.d.ts +221 -0
- package/dist/meta.js +213 -0
- 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
|
*/
|
|
@@ -164,7 +146,7 @@ export declare class RondevuAPI {
|
|
|
164
146
|
getOfferAnswer(offerId: string): Promise<{
|
|
165
147
|
sdp: string;
|
|
166
148
|
offerId: string;
|
|
167
|
-
|
|
149
|
+
answererPublicKey: string;
|
|
168
150
|
answeredAt: number;
|
|
169
151
|
matchedTags?: string[];
|
|
170
152
|
} | null>;
|
|
@@ -174,7 +156,7 @@ export declare class RondevuAPI {
|
|
|
174
156
|
poll(since?: number): Promise<{
|
|
175
157
|
answers: Array<{
|
|
176
158
|
offerId: string;
|
|
177
|
-
|
|
159
|
+
answererPublicKey: string;
|
|
178
160
|
sdp: string;
|
|
179
161
|
answeredAt: number;
|
|
180
162
|
matchedTags?: string[];
|
|
@@ -182,7 +164,7 @@ export declare class RondevuAPI {
|
|
|
182
164
|
iceCandidates: Record<string, Array<{
|
|
183
165
|
candidate: RTCIceCandidateInit | null;
|
|
184
166
|
role: 'offerer' | 'answerer';
|
|
185
|
-
|
|
167
|
+
peerPublicKey: string;
|
|
186
168
|
createdAt: number;
|
|
187
169
|
}>>;
|
|
188
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)
|
|
@@ -394,10 +296,7 @@ export class RondevuAPI {
|
|
|
394
296
|
};
|
|
395
297
|
}
|
|
396
298
|
}
|
|
397
|
-
//
|
|
398
|
-
RondevuAPI.
|
|
399
|
-
RondevuAPI.
|
|
400
|
-
RondevuAPI.DEFAULT_CREDENTIAL_NAME_MAX_LENGTH = 128;
|
|
401
|
-
RondevuAPI.DEFAULT_SECRET_MIN_LENGTH = 64; // 256 bits
|
|
402
|
-
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
|
|
403
302
|
RondevuAPI.MAX_CANONICALIZE_DEPTH = 100; // Prevent stack overflow
|
|
@@ -7,7 +7,7 @@ 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;
|
|
@@ -21,7 +21,7 @@ export interface AnswererOptions {
|
|
|
21
21
|
*/
|
|
22
22
|
export declare class AnswererConnection extends RondevuConnection {
|
|
23
23
|
private api;
|
|
24
|
-
private
|
|
24
|
+
private ownerPublicKey;
|
|
25
25
|
private tags;
|
|
26
26
|
private offerId;
|
|
27
27
|
private offerSdp;
|
|
@@ -40,15 +40,15 @@ export declare class AnswererConnection extends RondevuConnection {
|
|
|
40
40
|
*/
|
|
41
41
|
protected getApi(): any;
|
|
42
42
|
/**
|
|
43
|
-
* Get the owner
|
|
43
|
+
* Get the owner public key (implements abstract method)
|
|
44
44
|
*/
|
|
45
|
-
protected
|
|
45
|
+
protected getOwnerPublicKey(): string;
|
|
46
46
|
/**
|
|
47
47
|
* Answerers accept ICE candidates from offerers only
|
|
48
48
|
*/
|
|
49
49
|
protected getIceCandidateRole(): 'offerer' | null;
|
|
50
50
|
/**
|
|
51
|
-
* Attempt to reconnect to the same
|
|
51
|
+
* Attempt to reconnect to the same peer
|
|
52
52
|
*/
|
|
53
53
|
protected attemptReconnect(): void;
|
|
54
54
|
/**
|
|
@@ -10,7 +10,7 @@ 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;
|
|
@@ -76,10 +76,10 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
76
76
|
return this.api;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* Get the owner
|
|
79
|
+
* Get the owner public key (implements abstract method)
|
|
80
80
|
*/
|
|
81
|
-
|
|
82
|
-
return this.
|
|
81
|
+
getOwnerPublicKey() {
|
|
82
|
+
return this.ownerPublicKey;
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Answerers accept ICE candidates from offerers only
|
|
@@ -88,11 +88,11 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
88
88
|
return 'offerer';
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* Attempt to reconnect to the same
|
|
91
|
+
* Attempt to reconnect to the same peer
|
|
92
92
|
*/
|
|
93
93
|
attemptReconnect() {
|
|
94
|
-
this.debug(`Attempting to reconnect to ${this.
|
|
95
|
-
// 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
|
|
96
96
|
// Clean up old connection
|
|
97
97
|
if (this.pc) {
|
|
98
98
|
this.pc.close();
|
|
@@ -110,16 +110,16 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
110
110
|
if (!response || !response.offers || response.offers.length === 0) {
|
|
111
111
|
throw new Error('No offers available for reconnection');
|
|
112
112
|
}
|
|
113
|
-
// Filter for offers from the same
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
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}`);
|
|
117
117
|
}
|
|
118
|
-
// Pick a random offer from the same
|
|
119
|
-
const offer =
|
|
118
|
+
// Pick a random offer from the same peer
|
|
119
|
+
const offer = peerOffers[Math.floor(Math.random() * peerOffers.length)];
|
|
120
120
|
this.offerId = offer.offerId;
|
|
121
121
|
this.offerSdp = offer.sdp;
|
|
122
|
-
this.debug(`Found new offer ${offer.offerId} from ${this.
|
|
122
|
+
this.debug(`Found new offer ${offer.offerId} from ${this.ownerPublicKey}`);
|
|
123
123
|
// Reinitialize with new offer
|
|
124
124
|
return this.initialize();
|
|
125
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
|
*/
|
|
@@ -7,7 +7,7 @@ import { ConnectionConfig } from './config.js';
|
|
|
7
7
|
import { WebRTCAdapter } from '../webrtc/adapter.js';
|
|
8
8
|
export interface OffererOptions {
|
|
9
9
|
api: RondevuAPI;
|
|
10
|
-
|
|
10
|
+
ownerPublicKey: string;
|
|
11
11
|
offerId: string;
|
|
12
12
|
pc: RTCPeerConnection;
|
|
13
13
|
dc?: RTCDataChannel;
|
|
@@ -19,9 +19,9 @@ export interface OffererOptions {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare class OffererConnection extends RondevuConnection {
|
|
21
21
|
private api;
|
|
22
|
-
private
|
|
22
|
+
private ownerPublicKey;
|
|
23
23
|
private offerId;
|
|
24
|
-
private
|
|
24
|
+
private _peerPublicKey;
|
|
25
25
|
private rotationLock;
|
|
26
26
|
private rotating;
|
|
27
27
|
private rotationAttempts;
|
|
@@ -35,7 +35,7 @@ export declare class OffererConnection extends RondevuConnection {
|
|
|
35
35
|
/**
|
|
36
36
|
* Process an answer from the answerer
|
|
37
37
|
*/
|
|
38
|
-
processAnswer(sdp: string,
|
|
38
|
+
processAnswer(sdp: string, answererPublicKey: string): Promise<void>;
|
|
39
39
|
/**
|
|
40
40
|
* Rebind this connection to a new offer (when previous offer failed)
|
|
41
41
|
* Keeps the same connection object alive but with new underlying WebRTC
|
|
@@ -62,9 +62,9 @@ export declare class OffererConnection extends RondevuConnection {
|
|
|
62
62
|
*/
|
|
63
63
|
protected getApi(): any;
|
|
64
64
|
/**
|
|
65
|
-
* Get the owner
|
|
65
|
+
* Get the owner public key
|
|
66
66
|
*/
|
|
67
|
-
protected
|
|
67
|
+
protected getOwnerPublicKey(): string;
|
|
68
68
|
/**
|
|
69
69
|
* Offerers accept all ICE candidates (no filtering)
|
|
70
70
|
*/
|
|
@@ -92,10 +92,10 @@ export declare class OffererConnection extends RondevuConnection {
|
|
|
92
92
|
*/
|
|
93
93
|
getOfferId(): string;
|
|
94
94
|
/**
|
|
95
|
-
* Get the peer
|
|
95
|
+
* Get the peer public key (who answered this offer)
|
|
96
96
|
* Returns null if no answer has been processed yet
|
|
97
97
|
*/
|
|
98
|
-
get
|
|
98
|
+
get peerPublicKey(): string | null;
|
|
99
99
|
/**
|
|
100
100
|
* Handle remote ICE candidates received from polling
|
|
101
101
|
* Called by OfferPool when poll:ice event is received
|