mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-15 05:13:23 +00:00
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:
174
src/api.ts
174
src/api.ts
@@ -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,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export type {
|
|||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
Credentials,
|
|
||||||
Keypair,
|
Keypair,
|
||||||
OfferRequest,
|
OfferRequest,
|
||||||
Offer,
|
Offer,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
102
src/rondevu.ts
102
src/rondevu.ts
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user