@xtr-dev/rondevu-client 0.7.11 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ import { RondevuUsername } from './usernames.js';
2
+ import RondevuPeer from './peer/index.js';
3
+ import { RondevuOffers } from './offers.js';
4
+ import { ServicePool } from './service-pool.js';
5
+ /**
6
+ * Rondevu Services API
7
+ * Handles service publishing and management
8
+ */
9
+ export class RondevuServices {
10
+ constructor(baseUrl, credentials) {
11
+ this.baseUrl = baseUrl;
12
+ this.credentials = credentials;
13
+ this.usernameApi = new RondevuUsername(baseUrl);
14
+ this.offersApi = new RondevuOffers(baseUrl, credentials);
15
+ }
16
+ /**
17
+ * Publishes a service
18
+ */
19
+ async publishService(options) {
20
+ const { username, privateKey, serviceFqn, rtcConfig, isPublic = false, metadata, ttl } = options;
21
+ // Validate FQN format
22
+ this.validateServiceFqn(serviceFqn);
23
+ // Create WebRTC peer connection to generate offer
24
+ const pc = new RTCPeerConnection(rtcConfig || {
25
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
26
+ });
27
+ // Add a data channel (required for datachannel-based services)
28
+ pc.createDataChannel('rondevu-service');
29
+ // Create offer
30
+ const offer = await pc.createOffer();
31
+ await pc.setLocalDescription(offer);
32
+ if (!offer.sdp) {
33
+ throw new Error('Failed to generate SDP');
34
+ }
35
+ // Create signature for username verification
36
+ const timestamp = Date.now();
37
+ const message = `publish:${username}:${serviceFqn}:${timestamp}`;
38
+ const signature = await this.usernameApi.signMessage(message, privateKey);
39
+ // Publish service
40
+ const response = await fetch(`${this.baseUrl}/services`, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
45
+ },
46
+ body: JSON.stringify({
47
+ username,
48
+ serviceFqn,
49
+ sdp: offer.sdp,
50
+ ttl,
51
+ isPublic,
52
+ metadata,
53
+ signature,
54
+ message
55
+ })
56
+ });
57
+ if (!response.ok) {
58
+ const error = await response.json();
59
+ pc.close();
60
+ throw new Error(error.error || 'Failed to publish service');
61
+ }
62
+ const data = await response.json();
63
+ // Close the connection for now (would be kept open in a real implementation)
64
+ pc.close();
65
+ return {
66
+ serviceId: data.serviceId,
67
+ uuid: data.uuid,
68
+ offerId: data.offerId,
69
+ expiresAt: data.expiresAt
70
+ };
71
+ }
72
+ /**
73
+ * Unpublishes a service
74
+ */
75
+ async unpublishService(serviceId, username) {
76
+ const response = await fetch(`${this.baseUrl}/services/${serviceId}`, {
77
+ method: 'DELETE',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
81
+ },
82
+ body: JSON.stringify({ username })
83
+ });
84
+ if (!response.ok) {
85
+ const error = await response.json();
86
+ throw new Error(error.error || 'Failed to unpublish service');
87
+ }
88
+ }
89
+ /**
90
+ * Exposes a service with an automatic connection handler
91
+ * This is a convenience method that publishes the service and manages connections
92
+ *
93
+ * Set poolSize > 1 to enable offer pooling for handling multiple concurrent connections
94
+ */
95
+ async exposeService(options) {
96
+ const { username, privateKey, serviceFqn, rtcConfig, isPublic, metadata, ttl, handler, poolSize, pollingInterval, onPoolStatus, onError } = options;
97
+ // If poolSize > 1, use pooled implementation
98
+ if (poolSize && poolSize > 1) {
99
+ const pool = new ServicePool(this.baseUrl, this.credentials, {
100
+ username,
101
+ privateKey,
102
+ serviceFqn,
103
+ rtcConfig,
104
+ isPublic,
105
+ metadata,
106
+ ttl,
107
+ handler: (channel, peer, connectionId) => handler(channel, peer, connectionId),
108
+ poolSize,
109
+ pollingInterval,
110
+ onPoolStatus,
111
+ onError
112
+ });
113
+ return await pool.start();
114
+ }
115
+ // Otherwise, use existing single-offer logic (UNCHANGED)
116
+ // Validate FQN
117
+ this.validateServiceFqn(serviceFqn);
118
+ // Create peer connection
119
+ const pc = new RTCPeerConnection(rtcConfig || {
120
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
121
+ });
122
+ // Create data channel
123
+ const channel = pc.createDataChannel('rondevu-service');
124
+ // Set up handler
125
+ channel.onopen = () => {
126
+ const peer = new RondevuPeer(this.offersApi, rtcConfig || {
127
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
128
+ });
129
+ handler(channel, peer);
130
+ };
131
+ // Create offer
132
+ const offer = await pc.createOffer();
133
+ await pc.setLocalDescription(offer);
134
+ if (!offer.sdp) {
135
+ pc.close();
136
+ throw new Error('Failed to generate SDP');
137
+ }
138
+ // Create signature
139
+ const timestamp = Date.now();
140
+ const message = `publish:${username}:${serviceFqn}:${timestamp}`;
141
+ const signature = await this.usernameApi.signMessage(message, privateKey);
142
+ // Publish service
143
+ const response = await fetch(`${this.baseUrl}/services`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
148
+ },
149
+ body: JSON.stringify({
150
+ username,
151
+ serviceFqn,
152
+ sdp: offer.sdp,
153
+ ttl,
154
+ isPublic,
155
+ metadata,
156
+ signature,
157
+ message
158
+ })
159
+ });
160
+ if (!response.ok) {
161
+ const error = await response.json();
162
+ pc.close();
163
+ throw new Error(error.error || 'Failed to expose service');
164
+ }
165
+ const data = await response.json();
166
+ return {
167
+ serviceId: data.serviceId,
168
+ uuid: data.uuid,
169
+ offerId: data.offerId,
170
+ unpublish: () => this.unpublishService(data.serviceId, username)
171
+ };
172
+ }
173
+ /**
174
+ * Validates service FQN format
175
+ */
176
+ validateServiceFqn(fqn) {
177
+ const parts = fqn.split('@');
178
+ if (parts.length !== 2) {
179
+ throw new Error('Service FQN must be in format: service-name@version');
180
+ }
181
+ const [serviceName, version] = parts;
182
+ // Validate service name (reverse domain notation)
183
+ const serviceNameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
184
+ if (!serviceNameRegex.test(serviceName)) {
185
+ throw new Error('Service name must be reverse domain notation (e.g., com.example.service)');
186
+ }
187
+ if (serviceName.length < 3 || serviceName.length > 128) {
188
+ throw new Error('Service name must be 3-128 characters');
189
+ }
190
+ // Validate version (semantic versioning)
191
+ const versionRegex = /^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$/;
192
+ if (!versionRegex.test(version)) {
193
+ throw new Error('Version must be semantic versioning (e.g., 1.0.0, 2.1.3-beta)');
194
+ }
195
+ }
196
+ /**
197
+ * Parses a service FQN into name and version
198
+ */
199
+ parseServiceFqn(fqn) {
200
+ const parts = fqn.split('@');
201
+ if (parts.length !== 2) {
202
+ throw new Error('Invalid FQN format');
203
+ }
204
+ return { name: parts[0], version: parts[1] };
205
+ }
206
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Username claim result
3
+ */
4
+ export interface UsernameClaimResult {
5
+ username: string;
6
+ publicKey: string;
7
+ privateKey: string;
8
+ claimedAt: number;
9
+ expiresAt: number;
10
+ }
11
+ /**
12
+ * Username availability check result
13
+ */
14
+ export interface UsernameCheckResult {
15
+ username: string;
16
+ available: boolean;
17
+ claimedAt?: number;
18
+ expiresAt?: number;
19
+ publicKey?: string;
20
+ }
21
+ /**
22
+ * Rondevu Username API
23
+ * Handles username claiming with Ed25519 cryptographic proof
24
+ */
25
+ export declare class RondevuUsername {
26
+ private baseUrl;
27
+ constructor(baseUrl: string);
28
+ /**
29
+ * Generates an Ed25519 keypair for username claiming
30
+ */
31
+ generateKeypair(): Promise<{
32
+ publicKey: string;
33
+ privateKey: string;
34
+ }>;
35
+ /**
36
+ * Signs a message with an Ed25519 private key
37
+ */
38
+ signMessage(message: string, privateKeyBase64: string): Promise<string>;
39
+ /**
40
+ * Claims a username
41
+ * Generates a new keypair if one is not provided
42
+ */
43
+ claimUsername(username: string, existingKeypair?: {
44
+ publicKey: string;
45
+ privateKey: string;
46
+ }): Promise<UsernameClaimResult>;
47
+ /**
48
+ * Checks if a username is available
49
+ */
50
+ checkUsername(username: string): Promise<UsernameCheckResult>;
51
+ /**
52
+ * Helper: Save keypair to localStorage
53
+ * WARNING: This stores the private key in localStorage which is not the most secure
54
+ * For production use, consider using IndexedDB with encryption or hardware security modules
55
+ */
56
+ saveKeypairToStorage(username: string, publicKey: string, privateKey: string): void;
57
+ /**
58
+ * Helper: Load keypair from localStorage
59
+ */
60
+ loadKeypairFromStorage(username: string): {
61
+ publicKey: string;
62
+ privateKey: string;
63
+ } | null;
64
+ /**
65
+ * Helper: Delete keypair from localStorage
66
+ */
67
+ deleteKeypairFromStorage(username: string): void;
68
+ /**
69
+ * Export keypair as JSON string (for backup)
70
+ */
71
+ exportKeypair(publicKey: string, privateKey: string): string;
72
+ /**
73
+ * Import keypair from JSON string
74
+ */
75
+ importKeypair(json: string): {
76
+ publicKey: string;
77
+ privateKey: string;
78
+ };
79
+ }
@@ -0,0 +1,147 @@
1
+ import * as ed25519 from '@noble/ed25519';
2
+ /**
3
+ * Convert Uint8Array to base64 string
4
+ */
5
+ function bytesToBase64(bytes) {
6
+ const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');
7
+ return btoa(binString);
8
+ }
9
+ /**
10
+ * Convert base64 string to Uint8Array
11
+ */
12
+ function base64ToBytes(base64) {
13
+ const binString = atob(base64);
14
+ return Uint8Array.from(binString, (char) => char.codePointAt(0));
15
+ }
16
+ /**
17
+ * Rondevu Username API
18
+ * Handles username claiming with Ed25519 cryptographic proof
19
+ */
20
+ export class RondevuUsername {
21
+ constructor(baseUrl) {
22
+ this.baseUrl = baseUrl;
23
+ }
24
+ /**
25
+ * Generates an Ed25519 keypair for username claiming
26
+ */
27
+ async generateKeypair() {
28
+ const privateKey = ed25519.utils.randomSecretKey();
29
+ const publicKey = await ed25519.getPublicKey(privateKey);
30
+ return {
31
+ publicKey: bytesToBase64(publicKey),
32
+ privateKey: bytesToBase64(privateKey)
33
+ };
34
+ }
35
+ /**
36
+ * Signs a message with an Ed25519 private key
37
+ */
38
+ async signMessage(message, privateKeyBase64) {
39
+ const privateKey = base64ToBytes(privateKeyBase64);
40
+ const encoder = new TextEncoder();
41
+ const messageBytes = encoder.encode(message);
42
+ const signature = await ed25519.sign(messageBytes, privateKey);
43
+ return bytesToBase64(signature);
44
+ }
45
+ /**
46
+ * Claims a username
47
+ * Generates a new keypair if one is not provided
48
+ */
49
+ async claimUsername(username, existingKeypair) {
50
+ // Generate or use existing keypair
51
+ const keypair = existingKeypair || await this.generateKeypair();
52
+ // Create signed message
53
+ const timestamp = Date.now();
54
+ const message = `claim:${username}:${timestamp}`;
55
+ const signature = await this.signMessage(message, keypair.privateKey);
56
+ // Send claim request
57
+ const response = await fetch(`${this.baseUrl}/usernames/claim`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ username,
62
+ publicKey: keypair.publicKey,
63
+ signature,
64
+ message
65
+ })
66
+ });
67
+ if (!response.ok) {
68
+ const error = await response.json();
69
+ throw new Error(error.error || 'Failed to claim username');
70
+ }
71
+ const data = await response.json();
72
+ return {
73
+ username: data.username,
74
+ publicKey: keypair.publicKey,
75
+ privateKey: keypair.privateKey,
76
+ claimedAt: data.claimedAt,
77
+ expiresAt: data.expiresAt
78
+ };
79
+ }
80
+ /**
81
+ * Checks if a username is available
82
+ */
83
+ async checkUsername(username) {
84
+ const response = await fetch(`${this.baseUrl}/usernames/${username}`);
85
+ if (!response.ok) {
86
+ throw new Error('Failed to check username');
87
+ }
88
+ const data = await response.json();
89
+ return {
90
+ username: data.username,
91
+ available: data.available,
92
+ claimedAt: data.claimedAt,
93
+ expiresAt: data.expiresAt,
94
+ publicKey: data.publicKey
95
+ };
96
+ }
97
+ /**
98
+ * Helper: Save keypair to localStorage
99
+ * WARNING: This stores the private key in localStorage which is not the most secure
100
+ * For production use, consider using IndexedDB with encryption or hardware security modules
101
+ */
102
+ saveKeypairToStorage(username, publicKey, privateKey) {
103
+ const data = { username, publicKey, privateKey, savedAt: Date.now() };
104
+ localStorage.setItem(`rondevu:keypair:${username}`, JSON.stringify(data));
105
+ }
106
+ /**
107
+ * Helper: Load keypair from localStorage
108
+ */
109
+ loadKeypairFromStorage(username) {
110
+ const stored = localStorage.getItem(`rondevu:keypair:${username}`);
111
+ if (!stored)
112
+ return null;
113
+ try {
114
+ const data = JSON.parse(stored);
115
+ return { publicKey: data.publicKey, privateKey: data.privateKey };
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ /**
122
+ * Helper: Delete keypair from localStorage
123
+ */
124
+ deleteKeypairFromStorage(username) {
125
+ localStorage.removeItem(`rondevu:keypair:${username}`);
126
+ }
127
+ /**
128
+ * Export keypair as JSON string (for backup)
129
+ */
130
+ exportKeypair(publicKey, privateKey) {
131
+ return JSON.stringify({
132
+ publicKey,
133
+ privateKey,
134
+ exportedAt: Date.now()
135
+ });
136
+ }
137
+ /**
138
+ * Import keypair from JSON string
139
+ */
140
+ importKeypair(json) {
141
+ const data = JSON.parse(json);
142
+ if (!data.publicKey || !data.privateKey) {
143
+ throw new Error('Invalid keypair format');
144
+ }
145
+ return { publicKey: data.publicKey, privateKey: data.privateKey };
146
+ }
147
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.7.11",
4
- "description": "TypeScript client for Rondevu topic-based peer discovery and signaling server",
3
+ "version": "0.8.0",
4
+ "description": "TypeScript client for Rondevu DNS-like WebRTC with username claiming and service discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -27,6 +27,6 @@
27
27
  "README.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@xtr-dev/rondevu-client": "^0.5.1"
30
+ "@noble/ed25519": "^3.0.0"
31
31
  }
32
32
  }