mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-13 04:13:25 +00:00
Simplify API: Remove explicit claiming, add Rondevu.connect(), simplify publishService
Breaking changes:
- Remove claimUsername() method - username claiming now fully implicit
- Remove initialize() method - replaced with static Rondevu.connect()
- Change publishService() parameter from serviceFqn to service (username auto-appended)
- Make constructor private - must use Rondevu.connect()
- Remove nullable types from keypair/api - always initialized after connect()
New pattern:
const rondevu = await Rondevu.connect({ apiUrl, username })
await rondevu.publishService({ service: 'chat:2.0.0', offers })
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
120
README.md
120
README.md
@@ -38,18 +38,13 @@ npm install @xtr-dev/rondevu-client
|
|||||||
```typescript
|
```typescript
|
||||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
// 1. Initialize (with username or anonymous)
|
// 1. Connect to Rondevu (generates keypair, username auto-claimed on first request)
|
||||||
const rondevu = new Rondevu({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu',
|
apiUrl: 'https://api.ronde.vu',
|
||||||
username: 'alice' // Or omit for anonymous username
|
username: 'alice' // Or omit for anonymous username
|
||||||
})
|
})
|
||||||
|
|
||||||
await rondevu.initialize() // Generates keypair automatically
|
// 2. Create WebRTC offer
|
||||||
|
|
||||||
// 2. Claim username (optional - anonymous users auto-claim)
|
|
||||||
await rondevu.claimUsername()
|
|
||||||
|
|
||||||
// 3. Create WebRTC offer
|
|
||||||
const pc = new RTCPeerConnection({
|
const pc = new RTCPeerConnection({
|
||||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||||
})
|
})
|
||||||
@@ -61,7 +56,7 @@ await pc.setLocalDescription(offer)
|
|||||||
|
|
||||||
// 4. Publish service
|
// 4. Publish service
|
||||||
const service = await rondevu.publishService({
|
const service = await rondevu.publishService({
|
||||||
serviceFqn: 'chat:1.0.0@alice',
|
service: 'chat:1.0.0',
|
||||||
offers: [{ sdp: offer.sdp }],
|
offers: [{ sdp: offer.sdp }],
|
||||||
ttl: 300000
|
ttl: 300000
|
||||||
})
|
})
|
||||||
@@ -120,15 +115,12 @@ dc.onopen = () => {
|
|||||||
```typescript
|
```typescript
|
||||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
// 1. Initialize
|
// 1. Connect to Rondevu
|
||||||
const rondevu = new Rondevu({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu',
|
apiUrl: 'https://api.ronde.vu',
|
||||||
username: 'bob'
|
username: 'bob'
|
||||||
})
|
})
|
||||||
|
|
||||||
await rondevu.initialize()
|
|
||||||
await rondevu.claimUsername()
|
|
||||||
|
|
||||||
// 2. Get service offer
|
// 2. Get service offer
|
||||||
const serviceData = await rondevu.getService('chat:1.0.0@alice')
|
const serviceData = await rondevu.getService('chat:1.0.0@alice')
|
||||||
|
|
||||||
@@ -201,11 +193,13 @@ Main class for all Rondevu operations.
|
|||||||
```typescript
|
```typescript
|
||||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
const rondevu = new Rondevu({
|
// Create and connect to Rondevu
|
||||||
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: string, // Signaling server URL
|
apiUrl: string, // Signaling server URL
|
||||||
username?: string, // Optional: your username (auto-generates anonymous if omitted)
|
username?: string, // Optional: your username (auto-generates anonymous if omitted)
|
||||||
keypair?: Keypair, // Optional: reuse existing keypair
|
keypair?: Keypair, // Optional: reuse existing keypair
|
||||||
cryptoAdapter?: CryptoAdapter // Optional: platform-specific crypto (defaults to WebCryptoAdapter)
|
cryptoAdapter?: CryptoAdapter // Optional: platform-specific crypto (defaults to WebCryptoAdapter)
|
||||||
|
batching?: BatcherOptions | false // Optional: RPC batching configuration
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -218,7 +212,7 @@ The client supports both browser and Node.js environments using crypto adapters:
|
|||||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
// WebCryptoAdapter is used by default - no configuration needed
|
// WebCryptoAdapter is used by default - no configuration needed
|
||||||
const rondevu = new Rondevu({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu',
|
apiUrl: 'https://api.ronde.vu',
|
||||||
username: 'alice'
|
username: 'alice'
|
||||||
})
|
})
|
||||||
@@ -228,13 +222,11 @@ const rondevu = new Rondevu({
|
|||||||
```typescript
|
```typescript
|
||||||
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
|
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
const rondevu = new Rondevu({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu',
|
apiUrl: 'https://api.ronde.vu',
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
cryptoAdapter: new NodeCryptoAdapter()
|
cryptoAdapter: new NodeCryptoAdapter()
|
||||||
})
|
})
|
||||||
|
|
||||||
await rondevu.initialize()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** Node.js support requires:
|
**Note:** Node.js support requires:
|
||||||
@@ -255,25 +247,17 @@ class CustomCryptoAdapter implements CryptoAdapter {
|
|||||||
randomBytes(length: number): Uint8Array { /* ... */ }
|
randomBytes(length: number): Uint8Array { /* ... */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const rondevu = new Rondevu({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu',
|
apiUrl: 'https://api.ronde.vu',
|
||||||
cryptoAdapter: new CustomCryptoAdapter()
|
cryptoAdapter: new CustomCryptoAdapter()
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Initialization
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Initialize (generates keypair if not provided, auto-claims anonymous usernames)
|
|
||||||
await rondevu.initialize(): Promise<void>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Username Management
|
#### Username Management
|
||||||
|
|
||||||
```typescript
|
Usernames are **automatically claimed** on the first authenticated request (like `publishService()`).
|
||||||
// Claim username with Ed25519 signature
|
|
||||||
await rondevu.claimUsername(): Promise<void>
|
|
||||||
|
|
||||||
|
```typescript
|
||||||
// Check if username is claimed (checks server)
|
// Check if username is claimed (checks server)
|
||||||
await rondevu.isUsernameClaimed(): Promise<boolean>
|
await rondevu.isUsernameClaimed(): Promise<boolean>
|
||||||
|
|
||||||
@@ -281,10 +265,10 @@ await rondevu.isUsernameClaimed(): Promise<boolean>
|
|||||||
rondevu.getUsername(): string
|
rondevu.getUsername(): string
|
||||||
|
|
||||||
// Get public key
|
// Get public key
|
||||||
rondevu.getPublicKey(): string | null
|
rondevu.getPublicKey(): string
|
||||||
|
|
||||||
// Get keypair (for backup/storage)
|
// Get keypair (for backup/storage)
|
||||||
rondevu.getKeypair(): Keypair | null
|
rondevu.getKeypair(): Keypair
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Service Publishing
|
#### Service Publishing
|
||||||
@@ -292,9 +276,9 @@ rondevu.getKeypair(): Keypair | null
|
|||||||
```typescript
|
```typescript
|
||||||
// Publish service with offers
|
// Publish service with offers
|
||||||
await rondevu.publishService({
|
await rondevu.publishService({
|
||||||
serviceFqn: string, // e.g., 'chat:1.0.0@alice'
|
service: string, // e.g., 'chat:1.0.0' (username auto-appended)
|
||||||
offers: Array<{ sdp: string }>,
|
offers: Array<{ sdp: string }>,
|
||||||
ttl?: number // Optional: milliseconds (default: 300000)
|
ttl?: number // Optional: milliseconds (default: 300000)
|
||||||
}): Promise<Service>
|
}): Promise<Service>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -452,13 +436,8 @@ await api.checkUsername(username: string): Promise<{
|
|||||||
expiresAt?: number
|
expiresAt?: number
|
||||||
}>
|
}>
|
||||||
|
|
||||||
// Claim username
|
// Note: Username claiming is now implicit - usernames are auto-claimed
|
||||||
await api.claimUsername(
|
// on first authenticated request to the server
|
||||||
username: string,
|
|
||||||
publicKey: string,
|
|
||||||
signature: string,
|
|
||||||
message: string
|
|
||||||
): Promise<{ success: boolean, username: string }>
|
|
||||||
|
|
||||||
// ... (all other HTTP endpoints)
|
// ... (all other HTTP endpoints)
|
||||||
```
|
```
|
||||||
@@ -527,18 +506,16 @@ interface PollingConfig {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Auto-generate anonymous username (format: anon-{timestamp}-{random})
|
// Auto-generate anonymous username (format: anon-{timestamp}-{random})
|
||||||
const rondevu = new Rondevu({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu'
|
apiUrl: 'https://api.ronde.vu'
|
||||||
// No username provided - will generate anonymous username
|
// No username provided - will generate anonymous username
|
||||||
})
|
})
|
||||||
|
|
||||||
await rondevu.initialize() // Auto-claims anonymous username
|
|
||||||
|
|
||||||
console.log(rondevu.getUsername()) // e.g., "anon-lx2w34-a3f501"
|
console.log(rondevu.getUsername()) // e.g., "anon-lx2w34-a3f501"
|
||||||
|
|
||||||
// Anonymous users behave exactly like regular users
|
// Anonymous users behave exactly like regular users
|
||||||
await rondevu.publishService({
|
await rondevu.publishService({
|
||||||
serviceFqn: `chat:1.0.0@${rondevu.getUsername()}`,
|
service: 'chat:1.0.0',
|
||||||
offers: [{ sdp: offerSdp }]
|
offers: [{ sdp: offerSdp }]
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -547,15 +524,12 @@ await rondevu.publishService({
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Save keypair and username to localStorage
|
// Save keypair and username to localStorage
|
||||||
const rondevu = new Rondevu({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu',
|
apiUrl: 'https://api.ronde.vu',
|
||||||
username: 'alice'
|
username: 'alice'
|
||||||
})
|
})
|
||||||
|
|
||||||
await rondevu.initialize()
|
// Save for later (username will be auto-claimed on first authenticated request)
|
||||||
await rondevu.claimUsername()
|
|
||||||
|
|
||||||
// Save for later
|
|
||||||
localStorage.setItem('rondevu-username', rondevu.getUsername())
|
localStorage.setItem('rondevu-username', rondevu.getUsername())
|
||||||
localStorage.setItem('rondevu-keypair', JSON.stringify(rondevu.getKeypair()))
|
localStorage.setItem('rondevu-keypair', JSON.stringify(rondevu.getKeypair()))
|
||||||
|
|
||||||
@@ -563,13 +537,11 @@ localStorage.setItem('rondevu-keypair', JSON.stringify(rondevu.getKeypair()))
|
|||||||
const savedUsername = localStorage.getItem('rondevu-username')
|
const savedUsername = localStorage.getItem('rondevu-username')
|
||||||
const savedKeypair = JSON.parse(localStorage.getItem('rondevu-keypair'))
|
const savedKeypair = JSON.parse(localStorage.getItem('rondevu-keypair'))
|
||||||
|
|
||||||
const rondevu2 = new Rondevu({
|
const rondevu2 = await Rondevu.connect({
|
||||||
apiUrl: 'https://api.ronde.vu',
|
apiUrl: 'https://api.ronde.vu',
|
||||||
username: savedUsername,
|
username: savedUsername,
|
||||||
keypair: savedKeypair
|
keypair: savedKeypair
|
||||||
})
|
})
|
||||||
|
|
||||||
await rondevu2.initialize() // Reuses keypair
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Discovery
|
### Service Discovery
|
||||||
@@ -603,7 +575,7 @@ for (let i = 0; i < 5; i++) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const service = await rondevu.publishService({
|
const service = await rondevu.publishService({
|
||||||
serviceFqn: 'chat:1.0.0@alice',
|
service: 'chat:1.0.0',
|
||||||
offers,
|
offers,
|
||||||
ttl: 300000
|
ttl: 300000
|
||||||
})
|
})
|
||||||
@@ -666,7 +638,45 @@ const pc = new RTCPeerConnection()
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
See the [demo](https://github.com/xtr-dev/rondevu-demo) for a complete working example with React UI.
|
### Node.js Service Host Example
|
||||||
|
|
||||||
|
You can host WebRTC services in Node.js that browser clients can connect to. See the [Node.js Host Guide](../demo/NODE_HOST_GUIDE.md) for a complete guide.
|
||||||
|
|
||||||
|
**Quick example:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
|
||||||
|
import wrtc from 'wrtc'
|
||||||
|
|
||||||
|
const { RTCPeerConnection } = wrtc
|
||||||
|
|
||||||
|
// Initialize with Node crypto adapter
|
||||||
|
const rondevu = await Rondevu.connect({
|
||||||
|
apiUrl: 'https://api.ronde.vu',
|
||||||
|
username: 'mybot',
|
||||||
|
cryptoAdapter: new NodeCryptoAdapter()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create peer connection (offerer creates data channel)
|
||||||
|
const pc = new RTCPeerConnection(rtcConfig)
|
||||||
|
const dc = pc.createDataChannel('chat')
|
||||||
|
|
||||||
|
// Publish service (username auto-claimed on first publish)
|
||||||
|
const offer = await pc.createOffer()
|
||||||
|
await pc.setLocalDescription(offer)
|
||||||
|
|
||||||
|
await rondevu.publishService({
|
||||||
|
service: 'chat:1.0.0',
|
||||||
|
offers: [{ sdp: offer.sdp }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Browser clients can now discover and connect to chat:1.0.0@mybot
|
||||||
|
```
|
||||||
|
|
||||||
|
See complete examples:
|
||||||
|
- [Node.js Host Guide](../demo/NODE_HOST_GUIDE.md) - Full guide with complete examples
|
||||||
|
- [test-connect.js](../demo/test-connect.js) - Working Node.js client example
|
||||||
|
- [React Demo](https://github.com/xtr-dev/rondevu-demo) - Complete browser UI ([live](https://ronde.vu))
|
||||||
|
|
||||||
## Migration from v0.3.x
|
## Migration from v0.3.x
|
||||||
|
|
||||||
|
|||||||
28
src/api.ts
28
src/api.ts
@@ -157,7 +157,21 @@ export class RondevuAPI {
|
|||||||
|
|
||||||
const results: RpcResponse[] = await response.json()
|
const results: RpcResponse[] = await response.json()
|
||||||
|
|
||||||
|
// Validate response is an array
|
||||||
|
if (!Array.isArray(results)) {
|
||||||
|
console.error('Invalid RPC batch response:', results)
|
||||||
|
throw new Error('Server returned invalid batch response (not an array)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check response length matches request length
|
||||||
|
if (results.length !== requests.length) {
|
||||||
|
console.error(`Response length mismatch: expected ${requests.length}, got ${results.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
return results.map((result, i) => {
|
return results.map((result, i) => {
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
throw new Error(`Invalid response at index ${i}`)
|
||||||
|
}
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || `RPC call ${i} failed`)
|
throw new Error(result.error || `RPC call ${i} failed`)
|
||||||
}
|
}
|
||||||
@@ -223,20 +237,6 @@ export class RondevuAPI {
|
|||||||
return result.available
|
return result.available
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Claim a username
|
|
||||||
*/
|
|
||||||
async claimUsername(username: string, publicKey: string): Promise<void> {
|
|
||||||
const auth = await this.generateAuth('claim', username)
|
|
||||||
await this.rpc({
|
|
||||||
method: 'claimUsername',
|
|
||||||
message: auth.message,
|
|
||||||
signature: auth.signature,
|
|
||||||
publicKey: this.keypair.publicKey,
|
|
||||||
params: { username, publicKey },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if current username is claimed
|
* Check if current username is claimed
|
||||||
*/
|
*/
|
||||||
|
|||||||
202
src/rondevu.ts
202
src/rondevu.ts
@@ -10,7 +10,7 @@ export interface RondevuOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PublishServiceOptions {
|
export interface PublishServiceOptions {
|
||||||
serviceFqn: string // Must include @username (e.g., "chat:1.0.0@alice")
|
service: string // Service name and version (e.g., "chat:2.0.0") - username will be auto-appended
|
||||||
offers: Array<{ sdp: string }>
|
offers: Array<{ sdp: string }>
|
||||||
ttl?: number
|
ttl?: number
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export interface PublishServiceOptions {
|
|||||||
* Rondevu - Complete WebRTC signaling client
|
* Rondevu - Complete WebRTC signaling client
|
||||||
*
|
*
|
||||||
* Provides a unified API for:
|
* Provides a unified API for:
|
||||||
* - Username claiming with Ed25519 signatures
|
* - Implicit username claiming (auto-claimed on first authenticated request)
|
||||||
* - Service publishing with automatic signature generation
|
* - Service publishing with automatic signature generation
|
||||||
* - Service discovery (direct, random, paginated)
|
* - Service discovery (direct, random, paginated)
|
||||||
* - WebRTC signaling (offer/answer exchange, ICE relay)
|
* - WebRTC signaling (offer/answer exchange, ICE relay)
|
||||||
@@ -27,17 +27,15 @@ export interface PublishServiceOptions {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // Create Rondevu instance with username
|
* // Create and initialize Rondevu instance
|
||||||
* const rondevu = new Rondevu({
|
* const rondevu = await Rondevu.connect({
|
||||||
* apiUrl: 'https://signal.example.com',
|
* apiUrl: 'https://signal.example.com',
|
||||||
* username: 'alice',
|
* username: 'alice',
|
||||||
* })
|
* })
|
||||||
* await rondevu.initialize()
|
|
||||||
* await rondevu.claimUsername() // Claim username before publishing
|
|
||||||
*
|
*
|
||||||
* // Publish a service
|
* // Publish a service (username auto-claimed on first publish)
|
||||||
* const publishedService = await rondevu.publishService({
|
* const publishedService = await rondevu.publishService({
|
||||||
* serviceFqn: 'chat:1.0.0@alice',
|
* service: 'chat:1.0.0',
|
||||||
* offers: [{ sdp: offerSdp }],
|
* offers: [{ sdp: offerSdp }],
|
||||||
* ttl: 300000,
|
* ttl: 300000,
|
||||||
* })
|
* })
|
||||||
@@ -50,120 +48,106 @@ export interface PublishServiceOptions {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class Rondevu {
|
export class Rondevu {
|
||||||
private api: RondevuAPI | null = null
|
private api: RondevuAPI
|
||||||
private readonly apiUrl: string
|
private readonly apiUrl: string
|
||||||
private username: string
|
private username: string
|
||||||
private keypair: Keypair | null = null
|
private keypair: Keypair
|
||||||
private usernameClaimed = false
|
private usernameClaimed = false
|
||||||
private cryptoAdapter?: CryptoAdapter
|
private cryptoAdapter?: CryptoAdapter
|
||||||
private batchingOptions?: BatcherOptions | false
|
private batchingOptions?: BatcherOptions | false
|
||||||
|
|
||||||
constructor(options: RondevuOptions) {
|
private constructor(
|
||||||
this.apiUrl = options.apiUrl
|
apiUrl: string,
|
||||||
this.username = options.username || this.generateAnonymousUsername()
|
username: string,
|
||||||
this.keypair = options.keypair || null
|
keypair: Keypair,
|
||||||
this.cryptoAdapter = options.cryptoAdapter
|
api: RondevuAPI,
|
||||||
this.batchingOptions = options.batching
|
cryptoAdapter?: CryptoAdapter,
|
||||||
|
batchingOptions?: BatcherOptions | false
|
||||||
|
) {
|
||||||
|
this.apiUrl = apiUrl
|
||||||
|
this.username = username
|
||||||
|
this.keypair = keypair
|
||||||
|
this.api = api
|
||||||
|
this.cryptoAdapter = cryptoAdapter
|
||||||
|
this.batchingOptions = batchingOptions
|
||||||
|
|
||||||
console.log('[Rondevu] Constructor called:', {
|
console.log('[Rondevu] Instance created:', {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
hasKeypair: !!this.keypair,
|
publicKey: this.keypair.publicKey,
|
||||||
publicKey: this.keypair?.publicKey,
|
batchingEnabled: batchingOptions !== false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and initialize a Rondevu client
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const rondevu = await Rondevu.connect({
|
||||||
|
* apiUrl: 'https://api.ronde.vu',
|
||||||
|
* username: 'alice'
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static async connect(options: RondevuOptions): Promise<Rondevu> {
|
||||||
|
const username = options.username || Rondevu.generateAnonymousUsername()
|
||||||
|
|
||||||
|
console.log('[Rondevu] Connecting:', {
|
||||||
|
username,
|
||||||
|
hasKeypair: !!options.keypair,
|
||||||
batchingEnabled: options.batching !== false
|
batchingEnabled: options.batching !== false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Generate keypair if not provided
|
||||||
|
let keypair = options.keypair
|
||||||
|
if (!keypair) {
|
||||||
|
console.log('[Rondevu] Generating new keypair...')
|
||||||
|
keypair = await RondevuAPI.generateKeypair(options.cryptoAdapter)
|
||||||
|
console.log('[Rondevu] Generated keypair, publicKey:', keypair.publicKey)
|
||||||
|
} else {
|
||||||
|
console.log('[Rondevu] Using existing keypair, publicKey:', keypair.publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create API instance
|
||||||
|
const api = new RondevuAPI(
|
||||||
|
options.apiUrl,
|
||||||
|
username,
|
||||||
|
keypair,
|
||||||
|
options.cryptoAdapter,
|
||||||
|
options.batching
|
||||||
|
)
|
||||||
|
console.log('[Rondevu] Created API instance')
|
||||||
|
|
||||||
|
return new Rondevu(
|
||||||
|
options.apiUrl,
|
||||||
|
username,
|
||||||
|
keypair,
|
||||||
|
api,
|
||||||
|
options.cryptoAdapter,
|
||||||
|
options.batching
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an anonymous username with timestamp and random component
|
* Generate an anonymous username with timestamp and random component
|
||||||
*/
|
*/
|
||||||
private generateAnonymousUsername(): string {
|
private static generateAnonymousUsername(): string {
|
||||||
const timestamp = Date.now().toString(36)
|
const timestamp = Date.now().toString(36)
|
||||||
const random = Array.from(crypto.getRandomValues(new Uint8Array(3)))
|
const random = Array.from(crypto.getRandomValues(new Uint8Array(3)))
|
||||||
.map(b => b.toString(16).padStart(2, '0')).join('')
|
.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
return `anon-${timestamp}-${random}`
|
return `anon-${timestamp}-${random}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Initialization
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the service - generates keypair if not provided and creates API instance
|
|
||||||
* Call this before using other methods
|
|
||||||
*/
|
|
||||||
async initialize(): Promise<void> {
|
|
||||||
console.log('[Rondevu] Initialize called, hasKeypair:', !!this.keypair)
|
|
||||||
|
|
||||||
// Generate keypair if not provided
|
|
||||||
if (!this.keypair) {
|
|
||||||
console.log('[Rondevu] Generating new keypair...')
|
|
||||||
this.keypair = await RondevuAPI.generateKeypair(this.cryptoAdapter)
|
|
||||||
console.log('[Rondevu] Generated keypair, publicKey:', this.keypair.publicKey)
|
|
||||||
} else {
|
|
||||||
console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create API instance with username, keypair, crypto adapter, and batching options
|
|
||||||
this.api = new RondevuAPI(
|
|
||||||
this.apiUrl,
|
|
||||||
this.username,
|
|
||||||
this.keypair,
|
|
||||||
this.cryptoAdapter,
|
|
||||||
this.batchingOptions
|
|
||||||
)
|
|
||||||
console.log('[Rondevu] Created API instance with username:', this.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Username Management
|
// Username Management
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Claim the username with Ed25519 signature
|
|
||||||
* Should be called once before publishing services
|
|
||||||
*/
|
|
||||||
async claimUsername(): Promise<void> {
|
|
||||||
if (!this.keypair) {
|
|
||||||
throw new Error('Not initialized. Call initialize() first.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if username is already claimed
|
|
||||||
const available = await this.getAPI().isUsernameAvailable(this.username)
|
|
||||||
if (!available) {
|
|
||||||
// Check if it's claimed by us
|
|
||||||
const claimed = await this.getAPI().isUsernameClaimed()
|
|
||||||
if (claimed) {
|
|
||||||
this.usernameClaimed = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
throw new Error(`Username "${this.username}" is already claimed by another user`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Claim the username
|
|
||||||
await this.getAPI().claimUsername(this.username, this.keypair.publicKey)
|
|
||||||
this.usernameClaimed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get API instance (creates lazily if needed)
|
|
||||||
*/
|
|
||||||
private getAPI(): RondevuAPI {
|
|
||||||
if (!this.api) {
|
|
||||||
throw new Error('Not initialized. Call initialize() first.')
|
|
||||||
}
|
|
||||||
return this.api
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if username has been claimed (checks with server)
|
* Check if username has been claimed (checks with server)
|
||||||
*/
|
*/
|
||||||
async isUsernameClaimed(): Promise<boolean> {
|
async isUsernameClaimed(): Promise<boolean> {
|
||||||
if (!this.keypair) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const claimed = await this.getAPI().isUsernameClaimed()
|
const claimed = await this.api.isUsernameClaimed()
|
||||||
|
|
||||||
// Update internal flag to match server state
|
// Update internal flag to match server state
|
||||||
this.usernameClaimed = claimed
|
this.usernameClaimed = claimed
|
||||||
@@ -184,15 +168,14 @@ export class Rondevu {
|
|||||||
* Username will be automatically claimed on first publish if not already claimed
|
* Username will be automatically claimed on first publish if not already claimed
|
||||||
*/
|
*/
|
||||||
async publishService(options: PublishServiceOptions): Promise<Service> {
|
async publishService(options: PublishServiceOptions): Promise<Service> {
|
||||||
if (!this.keypair) {
|
const { service, offers, ttl } = options
|
||||||
throw new Error('Not initialized. Call initialize() first.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { serviceFqn, offers, ttl } = options
|
// Auto-append username to service
|
||||||
|
const serviceFqn = `${service}@${this.username}`
|
||||||
|
|
||||||
// Publish to server (server will auto-claim username if needed)
|
// Publish to server (server will auto-claim username if needed)
|
||||||
// Note: signature and message are generated by the API layer
|
// Note: signature and message are generated by the API layer
|
||||||
const result = await this.getAPI().publishService({
|
const result = await this.api.publishService({
|
||||||
serviceFqn,
|
serviceFqn,
|
||||||
offers,
|
offers,
|
||||||
ttl,
|
ttl,
|
||||||
@@ -223,7 +206,7 @@ export class Rondevu {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
expiresAt: number
|
expiresAt: number
|
||||||
}> {
|
}> {
|
||||||
return await this.getAPI().getService(serviceFqn)
|
return await this.api.getService(serviceFqn)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -239,7 +222,7 @@ export class Rondevu {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
expiresAt: number
|
expiresAt: number
|
||||||
}> {
|
}> {
|
||||||
return await this.getAPI().getService(serviceVersion)
|
return await this.api.getService(serviceVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,7 +243,7 @@ export class Rondevu {
|
|||||||
limit: number
|
limit: number
|
||||||
offset: number
|
offset: number
|
||||||
}> {
|
}> {
|
||||||
return await this.getAPI().getService(serviceVersion, { limit, offset })
|
return await this.api.getService(serviceVersion, { limit, offset })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -274,7 +257,7 @@ export class Rondevu {
|
|||||||
success: boolean
|
success: boolean
|
||||||
offerId: string
|
offerId: string
|
||||||
}> {
|
}> {
|
||||||
await this.getAPI().answerOffer(serviceFqn, offerId, sdp)
|
await this.api.answerOffer(serviceFqn, offerId, sdp)
|
||||||
return { success: true, offerId }
|
return { success: true, offerId }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +270,7 @@ export class Rondevu {
|
|||||||
answererId: string
|
answererId: string
|
||||||
answeredAt: number
|
answeredAt: number
|
||||||
} | null> {
|
} | null> {
|
||||||
return await this.getAPI().getOfferAnswer(serviceFqn, offerId)
|
return await this.api.getOfferAnswer(serviceFqn, offerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,7 +292,7 @@ export class Rondevu {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
}>>
|
}>>
|
||||||
}> {
|
}> {
|
||||||
return await this.getAPI().poll(since)
|
return await this.api.poll(since)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,7 +302,7 @@ export class Rondevu {
|
|||||||
count: number
|
count: number
|
||||||
offerId: string
|
offerId: string
|
||||||
}> {
|
}> {
|
||||||
return await this.getAPI().addOfferIceCandidates(serviceFqn, offerId, candidates)
|
return await this.api.addOfferIceCandidates(serviceFqn, offerId, candidates)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -329,7 +312,7 @@ export class Rondevu {
|
|||||||
candidates: IceCandidate[]
|
candidates: IceCandidate[]
|
||||||
offerId: string
|
offerId: string
|
||||||
}> {
|
}> {
|
||||||
return await this.getAPI().getOfferIceCandidates(serviceFqn, offerId, since)
|
return await this.api.getOfferIceCandidates(serviceFqn, offerId, since)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -339,7 +322,7 @@ export class Rondevu {
|
|||||||
/**
|
/**
|
||||||
* Get the current keypair (for backup/storage)
|
* Get the current keypair (for backup/storage)
|
||||||
*/
|
*/
|
||||||
getKeypair(): Keypair | null {
|
getKeypair(): Keypair {
|
||||||
return this.keypair
|
return this.keypair
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +336,8 @@ export class Rondevu {
|
|||||||
/**
|
/**
|
||||||
* Get the public key
|
* Get the public key
|
||||||
*/
|
*/
|
||||||
getPublicKey(): string | null {
|
getPublicKey(): string {
|
||||||
return this.keypair?.publicKey || null
|
return this.keypair.publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -362,9 +345,6 @@ export class Rondevu {
|
|||||||
* @deprecated Use direct methods on Rondevu instance instead
|
* @deprecated Use direct methods on Rondevu instance instead
|
||||||
*/
|
*/
|
||||||
getAPIPublic(): RondevuAPI {
|
getAPIPublic(): RondevuAPI {
|
||||||
if (!this.api) {
|
|
||||||
throw new Error('Not initialized. Call initialize() first.')
|
|
||||||
}
|
|
||||||
return this.api
|
return this.api
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user