Unified Ed25519 authentication - remove credentials system

BREAKING CHANGE: Remove credential-based authentication

- Remove Credentials interface and all credential-related code
- Remove register() method from RondevuAPI
- Remove setCredentials() and getAuthHeader() methods

RondevuAPI changes:
- Constructor now requires username and keypair (not credentials)
- Add generateAuthParams() helper for automatic signature generation
- All API methods now include {username, signature, message} auth
- POST requests: auth in body
- GET requests: auth in query params
- Remove Authorization header from all fetch calls

Rondevu class changes:
- Make username optional in RondevuOptions (auto-generates anon username)
- Make keypair optional (auto-generates if not provided)
- Add generateAnonymousUsername() method (anon-{timestamp}-{random})
- Update initialize() to create API with username+keypair (no register call)
- Auto-claim username for anonymous users during initialize()
- Add lazy getAPI() to ensure initialization

Message format for auth:
- Format: action:username:params:timestamp
- Examples: publishService:alice:chat:1.0.0@alice:1234567890
- Each request generates unique signature with timestamp

Index exports:
- Remove Credentials export (no longer exists)
This commit is contained in:
2025-12-10 22:07:07 +01:00
parent 239563ac5c
commit c9a5e0eae6
4 changed files with 168 additions and 117 deletions

View File

@@ -9,11 +9,6 @@ ed25519.hashes.sha512Async = async (message: Uint8Array) => {
return new Uint8Array(await crypto.subtle.digest('SHA-512', message as BufferSource)) return new Uint8Array(await crypto.subtle.digest('SHA-512', message as BufferSource))
} }
export interface Credentials {
peerId: string
secret: string
}
export interface Keypair { export interface Keypair {
publicKey: string publicKey: string
privateKey: string privateKey: string
@@ -92,26 +87,26 @@ function base64ToBytes(base64: string): Uint8Array {
export class RondevuAPI { export class RondevuAPI {
constructor( constructor(
private baseUrl: string, private baseUrl: string,
private credentials?: Credentials private username: string,
private keypair: Keypair
) {} ) {}
/** /**
* Set credentials for authentication * Generate authentication parameters (username, signature, message) for API calls
*/ */
setCredentials(credentials: Credentials): void { private async generateAuthParams(action: string, params: string = ''): Promise<{
this.credentials = credentials username: string;
} signature: string;
message: string;
}> {
const timestamp = Date.now();
const message = params
? `${action}:${this.username}:${params}:${timestamp}`
: `${action}:${this.username}:${timestamp}`;
/** const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
* Authentication header
*/ return { username: this.username, signature, message };
private getAuthHeader(): Record<string, string> {
if (!this.credentials) {
return {}
}
return {
Authorization: `Bearer ${this.credentials.peerId}:${this.credentials.secret}`,
}
} }
// ============================================ // ============================================
@@ -159,27 +154,6 @@ export class RondevuAPI {
return await ed25519.verifyAsync(signature, messageBytes, publicKey) return await ed25519.verifyAsync(signature, messageBytes, publicKey)
} }
// ============================================
// Authentication
// ============================================
/**
* Register a new peer and get credentials
*/
async register(): Promise<Credentials> {
const response = await fetch(`${this.baseUrl}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
throw new Error(`Registration failed: ${error.error || response.statusText}`)
}
return await response.json()
}
// ============================================ // ============================================
// Offers // Offers
// ============================================ // ============================================
@@ -188,13 +162,14 @@ export class RondevuAPI {
* Create one or more offers * Create one or more offers
*/ */
async createOffers(offers: OfferRequest[]): Promise<Offer[]> { async createOffers(offers: OfferRequest[]): Promise<Offer[]> {
const auth = await this.generateAuthParams('createOffers');
const response = await fetch(`${this.baseUrl}/offers`, { const response = await fetch(`${this.baseUrl}/offers`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...this.getAuthHeader(),
}, },
body: JSON.stringify({ offers }), body: JSON.stringify({ offers, ...auth }),
}) })
if (!response.ok) { if (!response.ok) {
@@ -209,9 +184,13 @@ export class RondevuAPI {
* Get offer by ID * Get offer by ID
*/ */
async getOffer(offerId: string): Promise<Offer> { async getOffer(offerId: string): Promise<Offer> {
const response = await fetch(`${this.baseUrl}/offers/${offerId}`, { const auth = await this.generateAuthParams('getOffer', offerId);
headers: this.getAuthHeader(), const url = new URL(`${this.baseUrl}/offers/${offerId}`);
}) url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -225,13 +204,14 @@ export class RondevuAPI {
* Answer a specific offer from a service * Answer a specific offer from a service
*/ */
async postOfferAnswer(serviceFqn: string, offerId: string, sdp: string): Promise<{ success: boolean; offerId: string }> { async postOfferAnswer(serviceFqn: string, offerId: string, sdp: string): Promise<{ success: boolean; offerId: string }> {
const auth = await this.generateAuthParams('answerOffer', `${serviceFqn}:${offerId}`);
const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/answer`, { const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/answer`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...this.getAuthHeader(),
}, },
body: JSON.stringify({ sdp }), body: JSON.stringify({ sdp, ...auth }),
}) })
if (!response.ok) { if (!response.ok) {
@@ -254,13 +234,17 @@ export class RondevuAPI {
answeredAt: number; answeredAt: number;
}>; }>;
}> { }> {
const url = since const auth = await this.generateAuthParams('getAnsweredOffers', since?.toString() || '');
? `${this.baseUrl}/offers/answered?since=${since}` const url = new URL(`${this.baseUrl}/offers/answered`);
: `${this.baseUrl}/offers/answered`;
const response = await fetch(url, { if (since) {
headers: this.getAuthHeader(), url.searchParams.set('since', since.toString());
}) }
url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -289,13 +273,17 @@ export class RondevuAPI {
createdAt: number; createdAt: number;
}>>; }>>;
}> { }> {
const url = since const auth = await this.generateAuthParams('pollOffers', since?.toString() || '');
? `${this.baseUrl}/offers/poll?since=${since}` const url = new URL(`${this.baseUrl}/offers/poll`);
: `${this.baseUrl}/offers/poll`;
const response = await fetch(url, { if (since) {
headers: this.getAuthHeader(), url.searchParams.set('since', since.toString());
}) }
url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -309,9 +297,13 @@ export class RondevuAPI {
* Get answer for a specific offer (offerer polls this) * Get answer for a specific offer (offerer polls this)
*/ */
async getOfferAnswer(serviceFqn: string, offerId: string): Promise<{ sdp: string; offerId: string; answererId: string; answeredAt: number } | null> { async getOfferAnswer(serviceFqn: string, offerId: string): Promise<{ sdp: string; offerId: string; answererId: string; answeredAt: number } | null> {
const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/answer`, { const auth = await this.generateAuthParams('getOfferAnswer', `${serviceFqn}:${offerId}`);
headers: this.getAuthHeader(), const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/answer`);
}) url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
// 404 means not yet answered // 404 means not yet answered
@@ -329,9 +321,14 @@ export class RondevuAPI {
* Search offers by topic * Search offers by topic
*/ */
async searchOffers(topic: string): Promise<Offer[]> { async searchOffers(topic: string): Promise<Offer[]> {
const response = await fetch(`${this.baseUrl}/offers?topic=${encodeURIComponent(topic)}`, { const auth = await this.generateAuthParams('searchOffers', topic);
headers: this.getAuthHeader(), const url = new URL(`${this.baseUrl}/offers`);
}) url.searchParams.set('topic', topic);
url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -349,13 +346,14 @@ export class RondevuAPI {
* Add ICE candidates to a specific offer * Add ICE candidates to a specific offer
*/ */
async addOfferIceCandidates(serviceFqn: string, offerId: string, candidates: RTCIceCandidateInit[]): Promise<{ count: number; offerId: string }> { async addOfferIceCandidates(serviceFqn: string, offerId: string, candidates: RTCIceCandidateInit[]): Promise<{ count: number; offerId: string }> {
const auth = await this.generateAuthParams('addIceCandidates', `${serviceFqn}:${offerId}`);
const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/ice-candidates`, { const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/ice-candidates`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...this.getAuthHeader(),
}, },
body: JSON.stringify({ candidates }), body: JSON.stringify({ candidates, ...auth }),
}) })
if (!response.ok) { if (!response.ok) {
@@ -370,10 +368,14 @@ export class RondevuAPI {
* Get ICE candidates for a specific offer (with polling support) * Get ICE candidates for a specific offer (with polling support)
*/ */
async getOfferIceCandidates(serviceFqn: string, offerId: string, since: number = 0): Promise<{ candidates: IceCandidate[]; offerId: string }> { async getOfferIceCandidates(serviceFqn: string, offerId: string, since: number = 0): Promise<{ candidates: IceCandidate[]; offerId: string }> {
const auth = await this.generateAuthParams('getIceCandidates', `${serviceFqn}:${offerId}:${since}`);
const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/ice-candidates`) const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/ice-candidates`)
url.searchParams.set('since', since.toString()) url.searchParams.set('since', since.toString())
url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString(), { headers: this.getAuthHeader() }) const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -396,13 +398,14 @@ export class RondevuAPI {
* Service FQN must include username: service:version@username * Service FQN must include username: service:version@username
*/ */
async publishService(service: ServiceRequest): Promise<Service> { async publishService(service: ServiceRequest): Promise<Service> {
const auth = await this.generateAuthParams('publishService', service.serviceFqn);
const response = await fetch(`${this.baseUrl}/services`, { const response = await fetch(`${this.baseUrl}/services`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...this.getAuthHeader(),
}, },
body: JSON.stringify(service), body: JSON.stringify({ ...service, username: auth.username }),
}) })
if (!response.ok) { if (!response.ok) {
@@ -418,9 +421,13 @@ export class RondevuAPI {
* Example: chat:1.0.0@alice * Example: chat:1.0.0@alice
*/ */
async getService(serviceFqn: string): Promise<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }> { async getService(serviceFqn: string): Promise<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }> {
const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}`, { const auth = await this.generateAuthParams('getService', serviceFqn);
headers: this.getAuthHeader(), const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}`);
}) url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -435,9 +442,13 @@ export class RondevuAPI {
* Example: chat:1.0.0 (without @username) * Example: chat:1.0.0 (without @username)
*/ */
async discoverService(serviceVersion: string): Promise<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }> { async discoverService(serviceVersion: string): Promise<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }> {
const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceVersion)}`, { const auth = await this.generateAuthParams('discoverService', serviceVersion);
headers: this.getAuthHeader(), const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceVersion)}`);
}) url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString())
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -452,13 +463,15 @@ export class RondevuAPI {
* Example: chat:1.0.0 (without @username) * Example: chat:1.0.0 (without @username)
*/ */
async discoverServices(serviceVersion: string, limit: number = 10, offset: number = 0): Promise<{ services: Array<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }>; count: number; limit: number; offset: number }> { async discoverServices(serviceVersion: string, limit: number = 10, offset: number = 0): Promise<{ services: Array<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }>; count: number; limit: number; offset: number }> {
const auth = await this.generateAuthParams('discoverServices', `${serviceVersion}:${limit}:${offset}`);
const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceVersion)}`) const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceVersion)}`)
url.searchParams.set('limit', limit.toString()) url.searchParams.set('limit', limit.toString())
url.searchParams.set('offset', offset.toString()) url.searchParams.set('offset', offset.toString())
url.searchParams.set('username', auth.username);
url.searchParams.set('signature', auth.signature);
url.searchParams.set('message', auth.message);
const response = await fetch(url.toString(), { const response = await fetch(url.toString())
headers: this.getAuthHeader(),
})
if (!response.ok) { if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' })) const error = await response.json().catch(() => ({ error: 'Unknown error' }))
@@ -502,7 +515,6 @@ export class RondevuAPI {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...this.getAuthHeader(),
}, },
body: JSON.stringify({ body: JSON.stringify({
publicKey, publicKey,

View File

@@ -14,7 +14,6 @@ export type {
} from './types.js' } from './types.js'
export type { export type {
Credentials,
Keypair, Keypair,
OfferRequest, OfferRequest,
Offer, Offer,

View File

@@ -111,7 +111,7 @@ export class RondevuSignaler implements Signaler {
} }
// Send answer to the service // Send answer to the service
const result = await this.rondevu.getAPI().postOfferAnswer(this.serviceFqn, this.offerId, answer.sdp) const result = await this.rondevu.getAPIPublic().postOfferAnswer(this.serviceFqn, this.offerId, answer.sdp)
this.offerId = result.offerId this.offerId = result.offerId
this.isOfferer = false this.isOfferer = false
@@ -173,7 +173,7 @@ export class RondevuSignaler implements Signaler {
} }
try { try {
await this.rondevu.getAPI().addOfferIceCandidates( await this.rondevu.getAPIPublic().addOfferIceCandidates(
this.serviceFqn, this.serviceFqn,
this.offerId, this.offerId,
[candidateData] [candidateData]
@@ -212,7 +212,7 @@ export class RondevuSignaler implements Signaler {
try { try {
// Get service by FQN (service should include @username) // Get service by FQN (service should include @username)
const serviceFqn = `${this.service}@${this.host}` const serviceFqn = `${this.service}@${this.host}`
const serviceData = await this.rondevu.getAPI().getService(serviceFqn) const serviceData = await this.rondevu.getAPIPublic().getService(serviceFqn)
if (!serviceData) { if (!serviceData) {
console.warn(`No service found for ${serviceFqn}`) console.warn(`No service found for ${serviceFqn}`)
@@ -390,7 +390,7 @@ export class RondevuSignaler implements Signaler {
try { try {
const result = await this.rondevu const result = await this.rondevu
.getAPI() .getAPIPublic()
.getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastPollTimestamp) .getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastPollTimestamp)
let foundCandidates = false let foundCandidates = false

View File

@@ -1,10 +1,9 @@
import { RondevuAPI, Credentials, Keypair, Service, ServiceRequest, IceCandidate } from './api.js' import { RondevuAPI, Keypair, Service, ServiceRequest, IceCandidate } from './api.js'
export interface RondevuOptions { export interface RondevuOptions {
apiUrl: string apiUrl: string
username: string username?: string // Optional, will generate anonymous if not provided
keypair?: Keypair keypair?: Keypair // Optional, will generate if not provided
credentials?: Credentials
} }
export interface PublishServiceOptions { export interface PublishServiceOptions {
@@ -22,19 +21,24 @@ export interface PublishServiceOptions {
* - Service discovery (direct, random, paginated) * - Service discovery (direct, random, paginated)
* - WebRTC signaling (offer/answer exchange, ICE relay) * - WebRTC signaling (offer/answer exchange, ICE relay)
* - Keypair management * - Keypair management
* - Anonymous usage (auto-generates username and keypair)
* *
* @example * @example
* ```typescript * ```typescript
* // Initialize (generates keypair automatically) * // Option 1: Named user (manually claim username)
* const rondevu = new Rondevu({ * const rondevu = new Rondevu({
* apiUrl: 'https://signal.example.com', * apiUrl: 'https://signal.example.com',
* username: 'alice', * username: 'alice',
* }) * })
*
* await rondevu.initialize() * await rondevu.initialize()
* await rondevu.claimUsername() // Claim username once
* *
* // Claim username (one time) * // Option 2: Anonymous user (auto-claims generated username)
* await rondevu.claimUsername() * const rondevu = new Rondevu({
* apiUrl: 'https://signal.example.com',
* // username omitted - will generate 'anon-xxxxx'
* })
* await rondevu.initialize() // Auto-claims anonymous username
* *
* // Publish a service * // Publish a service
* const publishedService = await rondevu.publishService({ * const publishedService = await rondevu.publishService({
@@ -51,15 +55,16 @@ export interface PublishServiceOptions {
* ``` * ```
*/ */
export class Rondevu { export class Rondevu {
private readonly api: RondevuAPI private api: RondevuAPI | null = null
private readonly username: string private readonly apiUrl: string
private username: string
private keypair: Keypair | null = null private keypair: Keypair | null = null
private usernameClaimed = false private usernameClaimed = false
constructor(options: RondevuOptions) { constructor(options: RondevuOptions) {
this.username = options.username this.apiUrl = options.apiUrl
this.username = options.username || this.generateAnonymousUsername()
this.keypair = options.keypair || null this.keypair = options.keypair || null
this.api = new RondevuAPI(options.apiUrl, options.credentials)
console.log('[Rondevu] Constructor called:', { console.log('[Rondevu] Constructor called:', {
username: this.username, username: this.username,
@@ -68,17 +73,29 @@ export class Rondevu {
}) })
} }
/**
* Generate an anonymous username with timestamp and random component
*/
private generateAnonymousUsername(): string {
const timestamp = Date.now().toString(36)
const random = Array.from(crypto.getRandomValues(new Uint8Array(3)))
.map(b => b.toString(16).padStart(2, '0')).join('')
return `anon-${timestamp}-${random}`
}
// ============================================ // ============================================
// Initialization // Initialization
// ============================================ // ============================================
/** /**
* Initialize the service - generates keypair if not provided * Initialize the service - generates keypair if not provided and creates API instance
* Auto-claims username for anonymous users
* Call this before using other methods * Call this before using other methods
*/ */
async initialize(): Promise<void> { async initialize(): Promise<void> {
console.log('[Rondevu] Initialize called, hasKeypair:', !!this.keypair) console.log('[Rondevu] Initialize called, hasKeypair:', !!this.keypair)
// Generate keypair if not provided
if (!this.keypair) { if (!this.keypair) {
console.log('[Rondevu] Generating new keypair...') console.log('[Rondevu] Generating new keypair...')
this.keypair = await RondevuAPI.generateKeypair() this.keypair = await RondevuAPI.generateKeypair()
@@ -87,10 +104,20 @@ export class Rondevu {
console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey) console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey)
} }
// Register with API if no credentials provided // Create API instance with username and keypair
if (!this.api['credentials']) { this.api = new RondevuAPI(this.apiUrl, this.username, this.keypair)
const credentials = await this.api.register() console.log('[Rondevu] Created API instance with username:', this.username)
this.api.setCredentials(credentials)
// Auto-claim username for anonymous users
if (this.username.startsWith('anon-')) {
console.log('[Rondevu] Auto-claiming anonymous username:', this.username)
try {
await this.claimUsername()
console.log('[Rondevu] Successfully claimed anonymous username')
} catch (error) {
console.error('[Rondevu] Failed to claim anonymous username:', error)
// Don't throw - allow the user to continue, they just won't be able to publish services
}
} }
} }
@@ -108,7 +135,7 @@ export class Rondevu {
} }
// Check if username is already claimed // Check if username is already claimed
const check = await this.api.checkUsername(this.username) const check = await this.getAPI().checkUsername(this.username)
if (!check.available) { if (!check.available) {
// Verify it's claimed by us // Verify it's claimed by us
if (check.publicKey === this.keypair.publicKey) { if (check.publicKey === this.keypair.publicKey) {
@@ -123,10 +150,20 @@ export class Rondevu {
const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey) const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey)
// Claim the username // Claim the username
await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message) await this.getAPI().claimUsername(this.username, this.keypair.publicKey, signature, message)
this.usernameClaimed = true 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)
*/ */
@@ -136,7 +173,7 @@ export class Rondevu {
} }
try { try {
const check = await this.api.checkUsername(this.username) const check = await this.getAPI().checkUsername(this.username)
// Debug logging // Debug logging
console.log('[Rondevu] Username check:', { console.log('[Rondevu] Username check:', {
@@ -194,7 +231,7 @@ export class Rondevu {
} }
// Publish to server // Publish to server
return await this.api.publishService(serviceRequest) return await this.getAPI().publishService(serviceRequest)
} }
// ============================================ // ============================================
@@ -214,7 +251,7 @@ export class Rondevu {
createdAt: number createdAt: number
expiresAt: number expiresAt: number
}> { }> {
return await this.api.getService(serviceFqn) return await this.getAPI().getService(serviceFqn)
} }
/** /**
@@ -230,7 +267,7 @@ export class Rondevu {
createdAt: number createdAt: number
expiresAt: number expiresAt: number
}> { }> {
return await this.api.discoverService(serviceVersion) return await this.getAPI().discoverService(serviceVersion)
} }
/** /**
@@ -251,7 +288,7 @@ export class Rondevu {
limit: number limit: number
offset: number offset: number
}> { }> {
return await this.api.discoverServices(serviceVersion, limit, offset) return await this.getAPI().discoverServices(serviceVersion, limit, offset)
} }
// ============================================ // ============================================
@@ -265,7 +302,7 @@ export class Rondevu {
success: boolean success: boolean
offerId: string offerId: string
}> { }> {
return await this.api.postOfferAnswer(serviceFqn, offerId, sdp) return await this.getAPI().postOfferAnswer(serviceFqn, offerId, sdp)
} }
/** /**
@@ -277,7 +314,7 @@ export class Rondevu {
answererId: string answererId: string
answeredAt: number answeredAt: number
} | null> { } | null> {
return await this.api.getOfferAnswer(serviceFqn, offerId) return await this.getAPI().getOfferAnswer(serviceFqn, offerId)
} }
/** /**
@@ -293,7 +330,7 @@ export class Rondevu {
answeredAt: number answeredAt: number
}> }>
}> { }> {
return await this.api.getAnsweredOffers(since) return await this.getAPI().getAnsweredOffers(since)
} }
/** /**
@@ -315,7 +352,7 @@ export class Rondevu {
createdAt: number createdAt: number
}>> }>>
}> { }> {
return await this.api.pollOffers(since) return await this.getAPI().pollOffers(since)
} }
/** /**
@@ -325,7 +362,7 @@ export class Rondevu {
count: number count: number
offerId: string offerId: string
}> { }> {
return await this.api.addOfferIceCandidates(serviceFqn, offerId, candidates) return await this.getAPI().addOfferIceCandidates(serviceFqn, offerId, candidates)
} }
/** /**
@@ -335,7 +372,7 @@ export class Rondevu {
candidates: IceCandidate[] candidates: IceCandidate[]
offerId: string offerId: string
}> { }> {
return await this.api.getOfferIceCandidates(serviceFqn, offerId, since) return await this.getAPI().getOfferIceCandidates(serviceFqn, offerId, since)
} }
// ============================================ // ============================================
@@ -367,7 +404,10 @@ export class Rondevu {
* Access to underlying API for advanced operations * Access to underlying API for advanced operations
* @deprecated Use direct methods on Rondevu instance instead * @deprecated Use direct methods on Rondevu instance instead
*/ */
getAPI(): RondevuAPI { getAPIPublic(): RondevuAPI {
if (!this.api) {
throw new Error('Not initialized. Call initialize() first.')
}
return this.api return this.api
} }
} }