@xtr-dev/rondevu-client 0.9.1 → 0.10.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.
Files changed (73) hide show
  1. package/dist/api.d.ts +147 -0
  2. package/dist/api.js +307 -0
  3. package/dist/bin.d.ts +35 -0
  4. package/dist/bin.js +35 -0
  5. package/dist/connection-manager.d.ts +104 -0
  6. package/dist/connection-manager.js +324 -0
  7. package/dist/connection.d.ts +112 -0
  8. package/dist/connection.js +194 -0
  9. package/dist/event-bus.d.ts +52 -0
  10. package/dist/event-bus.js +84 -0
  11. package/dist/index.d.ts +15 -11
  12. package/dist/index.js +9 -11
  13. package/dist/noop-signaler.d.ts +14 -0
  14. package/dist/noop-signaler.js +27 -0
  15. package/dist/rondevu-service.d.ts +81 -0
  16. package/dist/rondevu-service.js +131 -0
  17. package/dist/service-client.d.ts +92 -0
  18. package/dist/service-client.js +185 -0
  19. package/dist/service-host.d.ts +101 -0
  20. package/dist/service-host.js +185 -0
  21. package/dist/signaler.d.ts +25 -0
  22. package/dist/signaler.js +89 -0
  23. package/dist/types.d.ts +33 -0
  24. package/dist/types.js +2 -0
  25. package/dist/webrtc-context.d.ts +6 -0
  26. package/dist/webrtc-context.js +34 -0
  27. package/package.json +16 -2
  28. package/dist/auth.d.ts +0 -20
  29. package/dist/auth.js +0 -41
  30. package/dist/durable/channel.d.ts +0 -115
  31. package/dist/durable/channel.js +0 -301
  32. package/dist/durable/connection.d.ts +0 -125
  33. package/dist/durable/connection.js +0 -370
  34. package/dist/durable/reconnection.d.ts +0 -90
  35. package/dist/durable/reconnection.js +0 -127
  36. package/dist/durable/service.d.ts +0 -103
  37. package/dist/durable/service.js +0 -264
  38. package/dist/durable/types.d.ts +0 -149
  39. package/dist/durable/types.js +0 -28
  40. package/dist/event-emitter.d.ts +0 -54
  41. package/dist/event-emitter.js +0 -102
  42. package/dist/offer-pool.d.ts +0 -86
  43. package/dist/offer-pool.js +0 -145
  44. package/dist/offers.d.ts +0 -101
  45. package/dist/offers.js +0 -202
  46. package/dist/peer/answering-state.d.ts +0 -11
  47. package/dist/peer/answering-state.js +0 -39
  48. package/dist/peer/closed-state.d.ts +0 -8
  49. package/dist/peer/closed-state.js +0 -10
  50. package/dist/peer/connected-state.d.ts +0 -8
  51. package/dist/peer/connected-state.js +0 -11
  52. package/dist/peer/creating-offer-state.d.ts +0 -12
  53. package/dist/peer/creating-offer-state.js +0 -45
  54. package/dist/peer/exchanging-ice-state.d.ts +0 -17
  55. package/dist/peer/exchanging-ice-state.js +0 -64
  56. package/dist/peer/failed-state.d.ts +0 -10
  57. package/dist/peer/failed-state.js +0 -16
  58. package/dist/peer/idle-state.d.ts +0 -7
  59. package/dist/peer/idle-state.js +0 -14
  60. package/dist/peer/index.d.ts +0 -71
  61. package/dist/peer/index.js +0 -176
  62. package/dist/peer/state.d.ts +0 -23
  63. package/dist/peer/state.js +0 -63
  64. package/dist/peer/types.d.ts +0 -43
  65. package/dist/peer/types.js +0 -1
  66. package/dist/peer/waiting-for-answer-state.d.ts +0 -17
  67. package/dist/peer/waiting-for-answer-state.js +0 -60
  68. package/dist/rondevu.d.ts +0 -184
  69. package/dist/rondevu.js +0 -171
  70. package/dist/service-pool.d.ts +0 -123
  71. package/dist/service-pool.js +0 -417
  72. package/dist/usernames.d.ts +0 -79
  73. package/dist/usernames.js +0 -153
@@ -1,417 +0,0 @@
1
- import { RondevuOffers } from './offers.js';
2
- import { RondevuUsername } from './usernames.js';
3
- import RondevuPeer from './peer/index.js';
4
- import { OfferPool } from './offer-pool.js';
5
- /**
6
- * Manages a pooled service with multiple concurrent connections
7
- *
8
- * ServicePool coordinates offer creation, answer polling, and connection
9
- * management for services that need to handle multiple simultaneous connections.
10
- */
11
- export class ServicePool {
12
- constructor(baseUrl, credentials, options) {
13
- this.baseUrl = baseUrl;
14
- this.credentials = credentials;
15
- this.options = options;
16
- this.connections = new Map();
17
- this.peerConnections = new Map();
18
- this.status = {
19
- activeOffers: 0,
20
- activeConnections: 0,
21
- totalConnectionsHandled: 0,
22
- failedOfferCreations: 0
23
- };
24
- this.offersApi = new RondevuOffers(baseUrl, credentials);
25
- this.usernameApi = new RondevuUsername(baseUrl);
26
- }
27
- /**
28
- * Start the pooled service
29
- */
30
- async start() {
31
- const poolSize = this.options.poolSize || 1;
32
- // 1. Create initial service (publishes first offer)
33
- const service = await this.publishInitialService();
34
- this.serviceId = service.serviceId;
35
- this.uuid = service.uuid;
36
- // 2. Create additional offers for pool (poolSize - 1)
37
- const additionalOffers = [];
38
- const additionalPeerConnections = [];
39
- const additionalDataChannels = [];
40
- if (poolSize > 1) {
41
- try {
42
- const result = await this.createOffers(poolSize - 1);
43
- additionalOffers.push(...result.offers);
44
- additionalPeerConnections.push(...result.peerConnections);
45
- additionalDataChannels.push(...result.dataChannels);
46
- }
47
- catch (error) {
48
- this.handleError(error, 'initial-offer-creation');
49
- }
50
- }
51
- // 3. Initialize OfferPool with all offers
52
- this.offerPool = new OfferPool(this.offersApi, {
53
- poolSize,
54
- pollingInterval: this.options.pollingInterval || 2000,
55
- onAnswered: (answer) => this.handleConnection(answer),
56
- onRefill: (count) => this.createOffers(count),
57
- onError: (err, ctx) => this.handleError(err, ctx)
58
- });
59
- // Add all offers to pool with their peer connections and data channels
60
- const allOffers = [
61
- { id: service.offerId, peerId: this.credentials.peerId, sdp: service.offerSdp, topics: [], expiresAt: service.expiresAt, lastSeen: Date.now() },
62
- ...additionalOffers
63
- ];
64
- const allPeerConnections = [
65
- service.peerConnection,
66
- ...additionalPeerConnections
67
- ];
68
- const allDataChannels = [
69
- service.dataChannel,
70
- ...additionalDataChannels
71
- ];
72
- await this.offerPool.addOffers(allOffers, allPeerConnections, allDataChannels);
73
- // 4. Start polling
74
- await this.offerPool.start();
75
- // Update status
76
- this.updateStatus();
77
- // 5. Return handle
78
- return {
79
- serviceId: this.serviceId,
80
- uuid: this.uuid,
81
- offerId: service.offerId,
82
- unpublish: () => this.stop(),
83
- getStatus: () => this.getStatus(),
84
- addOffers: (count) => this.manualRefill(count)
85
- };
86
- }
87
- /**
88
- * Stop the pooled service and clean up
89
- */
90
- async stop() {
91
- // 1. Stop accepting new connections
92
- if (this.offerPool) {
93
- await this.offerPool.stop();
94
- }
95
- // 2. Close peer connections from the pool
96
- if (this.offerPool) {
97
- const poolPeerConnections = this.offerPool.getActivePeerConnections();
98
- poolPeerConnections.forEach(pc => {
99
- try {
100
- pc.close();
101
- }
102
- catch {
103
- // Ignore errors during cleanup
104
- }
105
- });
106
- }
107
- // 3. Delete remaining offers
108
- if (this.offerPool) {
109
- const offerIds = this.offerPool.getActiveOfferIds();
110
- await Promise.allSettled(offerIds.map(id => this.offersApi.delete(id).catch(() => { })));
111
- }
112
- // 4. Close active connections
113
- const closePromises = Array.from(this.connections.values()).map(async (conn) => {
114
- try {
115
- // Give a brief moment for graceful closure
116
- await new Promise(resolve => setTimeout(resolve, 100));
117
- conn.peer.pc.close();
118
- }
119
- catch {
120
- // Ignore errors during cleanup
121
- }
122
- });
123
- await Promise.allSettled(closePromises);
124
- // 5. Delete service if we have a serviceId
125
- if (this.serviceId) {
126
- try {
127
- const response = await fetch(`${this.baseUrl}/services/${this.serviceId}`, {
128
- method: 'DELETE',
129
- headers: {
130
- 'Content-Type': 'application/json',
131
- 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
132
- },
133
- body: JSON.stringify({ username: this.options.username })
134
- });
135
- if (!response.ok) {
136
- console.error('Failed to delete service:', await response.text());
137
- }
138
- }
139
- catch (error) {
140
- console.error('Error deleting service:', error);
141
- }
142
- }
143
- // Clear all state
144
- this.connections.clear();
145
- this.offerPool = undefined;
146
- }
147
- /**
148
- * Handle an answered offer by setting up the connection
149
- */
150
- async handleConnection(answer) {
151
- const connectionId = this.generateConnectionId();
152
- try {
153
- // Use the existing peer connection from the pool
154
- const peer = new RondevuPeer(this.offersApi, this.options.rtcConfig || {
155
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
156
- }, answer.peerConnection // Use the existing peer connection
157
- );
158
- peer.role = 'offerer';
159
- peer.offerId = answer.offerId;
160
- // Verify peer connection is in correct state
161
- if (peer.pc.signalingState !== 'have-local-offer') {
162
- console.error('Peer connection state info:', {
163
- signalingState: peer.pc.signalingState,
164
- connectionState: peer.pc.connectionState,
165
- iceConnectionState: peer.pc.iceConnectionState,
166
- iceGatheringState: peer.pc.iceGatheringState,
167
- hasLocalDescription: !!peer.pc.localDescription,
168
- hasRemoteDescription: !!peer.pc.remoteDescription,
169
- localDescriptionType: peer.pc.localDescription?.type,
170
- remoteDescriptionType: peer.pc.remoteDescription?.type,
171
- offerId: answer.offerId
172
- });
173
- throw new Error(`Invalid signaling state: ${peer.pc.signalingState}. Expected 'have-local-offer' to set remote answer.`);
174
- }
175
- // Set remote description (the answer)
176
- await peer.pc.setRemoteDescription({
177
- type: 'answer',
178
- sdp: answer.sdp
179
- });
180
- // Use the data channel we created when making the offer
181
- if (!answer.dataChannel) {
182
- throw new Error('No data channel found for answered offer');
183
- }
184
- const channel = answer.dataChannel;
185
- // Wait for the channel to open (it was created when we made the offer)
186
- if (channel.readyState !== 'open') {
187
- await new Promise((resolve, reject) => {
188
- const timeout = setTimeout(() => reject(new Error('Timeout waiting for data channel to open')), 30000);
189
- channel.onopen = () => {
190
- clearTimeout(timeout);
191
- resolve();
192
- };
193
- channel.onerror = (error) => {
194
- clearTimeout(timeout);
195
- reject(new Error('Data channel error'));
196
- };
197
- });
198
- }
199
- // Register connection
200
- this.connections.set(connectionId, {
201
- peer,
202
- channel,
203
- connectedAt: Date.now(),
204
- offerId: answer.offerId
205
- });
206
- this.status.activeConnections++;
207
- this.status.totalConnectionsHandled++;
208
- // Setup cleanup on disconnect
209
- peer.on('disconnected', () => {
210
- this.connections.delete(connectionId);
211
- this.status.activeConnections--;
212
- this.updateStatus();
213
- });
214
- peer.on('failed', () => {
215
- this.connections.delete(connectionId);
216
- this.status.activeConnections--;
217
- this.updateStatus();
218
- });
219
- // Update status
220
- this.updateStatus();
221
- // Invoke user handler (wrapped in try-catch)
222
- try {
223
- this.options.handler(channel, peer, connectionId);
224
- }
225
- catch (handlerError) {
226
- this.handleError(handlerError, 'handler');
227
- }
228
- }
229
- catch (error) {
230
- this.handleError(error, 'connection-setup');
231
- }
232
- }
233
- /**
234
- * Create multiple offers
235
- */
236
- async createOffers(count) {
237
- if (count <= 0) {
238
- return { offers: [], peerConnections: [], dataChannels: [] };
239
- }
240
- // Server supports max 10 offers per request
241
- const batchSize = Math.min(count, 10);
242
- const offers = [];
243
- const peerConnections = [];
244
- const dataChannels = [];
245
- try {
246
- // Create peer connections and generate offers
247
- const offerRequests = [];
248
- for (let i = 0; i < batchSize; i++) {
249
- const pc = new RTCPeerConnection(this.options.rtcConfig || {
250
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
251
- });
252
- // Create data channel (required for offers) and save reference
253
- const channel = pc.createDataChannel('rondevu-service');
254
- dataChannels.push(channel);
255
- // Create offer
256
- const offer = await pc.createOffer();
257
- await pc.setLocalDescription(offer);
258
- if (!offer.sdp) {
259
- pc.close();
260
- throw new Error('Failed to generate SDP');
261
- }
262
- offerRequests.push({
263
- sdp: offer.sdp,
264
- topics: [], // V2 doesn't use topics
265
- ttl: this.options.ttl
266
- });
267
- // Keep peer connection alive - DO NOT CLOSE
268
- peerConnections.push(pc);
269
- }
270
- // Batch create offers
271
- const createdOffers = await this.offersApi.create(offerRequests);
272
- offers.push(...createdOffers);
273
- // Set up ICE candidate handlers AFTER we have offer IDs
274
- for (let i = 0; i < peerConnections.length; i++) {
275
- const pc = peerConnections[i];
276
- const offerId = createdOffers[i].id;
277
- pc.onicecandidate = async (event) => {
278
- if (event.candidate) {
279
- const candidateData = event.candidate.toJSON();
280
- if (candidateData.candidate && candidateData.candidate !== '') {
281
- try {
282
- await this.offersApi.addIceCandidates(offerId, [candidateData]);
283
- }
284
- catch (err) {
285
- console.error('Error sending ICE candidate:', err);
286
- }
287
- }
288
- }
289
- };
290
- }
291
- }
292
- catch (error) {
293
- // Close any created peer connections on error
294
- peerConnections.forEach(pc => pc.close());
295
- this.status.failedOfferCreations++;
296
- this.handleError(error, 'offer-creation');
297
- throw error;
298
- }
299
- return { offers, peerConnections, dataChannels };
300
- }
301
- /**
302
- * Publish the initial service (creates first offer)
303
- */
304
- async publishInitialService() {
305
- const { username, privateKey, serviceFqn, rtcConfig, isPublic, metadata, ttl } = this.options;
306
- // Create peer connection for initial offer
307
- const pc = new RTCPeerConnection(rtcConfig || {
308
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
309
- });
310
- const dataChannel = pc.createDataChannel('rondevu-service');
311
- // Create offer
312
- const offer = await pc.createOffer();
313
- await pc.setLocalDescription(offer);
314
- if (!offer.sdp) {
315
- pc.close();
316
- throw new Error('Failed to generate SDP');
317
- }
318
- // Store the SDP
319
- const offerSdp = offer.sdp;
320
- // Create signature
321
- const timestamp = Date.now();
322
- const message = `publish:${username}:${serviceFqn}:${timestamp}`;
323
- const signature = await this.usernameApi.signMessage(message, privateKey);
324
- // Publish service
325
- const response = await fetch(`${this.baseUrl}/services`, {
326
- method: 'POST',
327
- headers: {
328
- 'Content-Type': 'application/json',
329
- 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
330
- },
331
- body: JSON.stringify({
332
- username,
333
- serviceFqn,
334
- sdp: offerSdp,
335
- ttl,
336
- isPublic,
337
- metadata,
338
- signature,
339
- message
340
- })
341
- });
342
- if (!response.ok) {
343
- pc.close();
344
- const error = await response.json();
345
- throw new Error(error.error || 'Failed to publish service');
346
- }
347
- const data = await response.json();
348
- // Set up ICE candidate handler now that we have the offer ID
349
- pc.onicecandidate = async (event) => {
350
- if (event.candidate) {
351
- const candidateData = event.candidate.toJSON();
352
- if (candidateData.candidate && candidateData.candidate !== '') {
353
- try {
354
- await this.offersApi.addIceCandidates(data.offerId, [candidateData]);
355
- }
356
- catch (err) {
357
- console.error('Error sending ICE candidate:', err);
358
- }
359
- }
360
- }
361
- };
362
- return {
363
- serviceId: data.serviceId,
364
- uuid: data.uuid,
365
- offerId: data.offerId,
366
- offerSdp,
367
- expiresAt: data.expiresAt,
368
- peerConnection: pc, // Keep peer connection alive
369
- dataChannel // Keep data channel alive
370
- };
371
- }
372
- /**
373
- * Manually add offers to the pool
374
- */
375
- async manualRefill(count) {
376
- if (!this.offerPool) {
377
- throw new Error('Pool not started');
378
- }
379
- const result = await this.createOffers(count);
380
- await this.offerPool.addOffers(result.offers, result.peerConnections, result.dataChannels);
381
- this.updateStatus();
382
- }
383
- /**
384
- * Get current pool status
385
- */
386
- getStatus() {
387
- return { ...this.status };
388
- }
389
- /**
390
- * Update status and notify listeners
391
- */
392
- updateStatus() {
393
- if (this.offerPool) {
394
- this.status.activeOffers = this.offerPool.getActiveOfferCount();
395
- }
396
- if (this.options.onPoolStatus) {
397
- this.options.onPoolStatus(this.getStatus());
398
- }
399
- }
400
- /**
401
- * Handle errors
402
- */
403
- handleError(error, context) {
404
- if (this.options.onError) {
405
- this.options.onError(error, context);
406
- }
407
- else {
408
- console.error(`ServicePool error (${context}):`, error);
409
- }
410
- }
411
- /**
412
- * Generate a unique connection ID
413
- */
414
- generateConnectionId() {
415
- return `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
416
- }
417
- }
@@ -1,79 +0,0 @@
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
- }
package/dist/usernames.js DELETED
@@ -1,153 +0,0 @@
1
- import * as ed25519 from '@noble/ed25519';
2
- // Set SHA-512 hash function for ed25519 (required in @noble/ed25519 v3+)
3
- // Uses built-in WebCrypto API which only provides async digest
4
- // We use the async ed25519 functions (signAsync, verifyAsync, getPublicKeyAsync)
5
- ed25519.hashes.sha512Async = async (message) => {
6
- return new Uint8Array(await crypto.subtle.digest('SHA-512', message));
7
- };
8
- /**
9
- * Convert Uint8Array to base64 string
10
- */
11
- function bytesToBase64(bytes) {
12
- const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');
13
- return btoa(binString);
14
- }
15
- /**
16
- * Convert base64 string to Uint8Array
17
- */
18
- function base64ToBytes(base64) {
19
- const binString = atob(base64);
20
- return Uint8Array.from(binString, (char) => char.codePointAt(0));
21
- }
22
- /**
23
- * Rondevu Username API
24
- * Handles username claiming with Ed25519 cryptographic proof
25
- */
26
- export class RondevuUsername {
27
- constructor(baseUrl) {
28
- this.baseUrl = baseUrl;
29
- }
30
- /**
31
- * Generates an Ed25519 keypair for username claiming
32
- */
33
- async generateKeypair() {
34
- const privateKey = ed25519.utils.randomSecretKey();
35
- const publicKey = await ed25519.getPublicKeyAsync(privateKey);
36
- return {
37
- publicKey: bytesToBase64(publicKey),
38
- privateKey: bytesToBase64(privateKey)
39
- };
40
- }
41
- /**
42
- * Signs a message with an Ed25519 private key
43
- */
44
- async signMessage(message, privateKeyBase64) {
45
- const privateKey = base64ToBytes(privateKeyBase64);
46
- const encoder = new TextEncoder();
47
- const messageBytes = encoder.encode(message);
48
- const signature = await ed25519.signAsync(messageBytes, privateKey);
49
- return bytesToBase64(signature);
50
- }
51
- /**
52
- * Claims a username
53
- * Generates a new keypair if one is not provided
54
- */
55
- async claimUsername(username, existingKeypair) {
56
- // Generate or use existing keypair
57
- const keypair = existingKeypair || await this.generateKeypair();
58
- // Create signed message
59
- const timestamp = Date.now();
60
- const message = `claim:${username}:${timestamp}`;
61
- const signature = await this.signMessage(message, keypair.privateKey);
62
- // Send claim request
63
- const response = await fetch(`${this.baseUrl}/usernames/claim`, {
64
- method: 'POST',
65
- headers: { 'Content-Type': 'application/json' },
66
- body: JSON.stringify({
67
- username,
68
- publicKey: keypair.publicKey,
69
- signature,
70
- message
71
- })
72
- });
73
- if (!response.ok) {
74
- const error = await response.json();
75
- throw new Error(error.error || 'Failed to claim username');
76
- }
77
- const data = await response.json();
78
- return {
79
- username: data.username,
80
- publicKey: keypair.publicKey,
81
- privateKey: keypair.privateKey,
82
- claimedAt: data.claimedAt,
83
- expiresAt: data.expiresAt
84
- };
85
- }
86
- /**
87
- * Checks if a username is available
88
- */
89
- async checkUsername(username) {
90
- const response = await fetch(`${this.baseUrl}/usernames/${username}`);
91
- if (!response.ok) {
92
- throw new Error('Failed to check username');
93
- }
94
- const data = await response.json();
95
- return {
96
- username: data.username,
97
- available: data.available,
98
- claimedAt: data.claimedAt,
99
- expiresAt: data.expiresAt,
100
- publicKey: data.publicKey
101
- };
102
- }
103
- /**
104
- * Helper: Save keypair to localStorage
105
- * WARNING: This stores the private key in localStorage which is not the most secure
106
- * For production use, consider using IndexedDB with encryption or hardware security modules
107
- */
108
- saveKeypairToStorage(username, publicKey, privateKey) {
109
- const data = { username, publicKey, privateKey, savedAt: Date.now() };
110
- localStorage.setItem(`rondevu:keypair:${username}`, JSON.stringify(data));
111
- }
112
- /**
113
- * Helper: Load keypair from localStorage
114
- */
115
- loadKeypairFromStorage(username) {
116
- const stored = localStorage.getItem(`rondevu:keypair:${username}`);
117
- if (!stored)
118
- return null;
119
- try {
120
- const data = JSON.parse(stored);
121
- return { publicKey: data.publicKey, privateKey: data.privateKey };
122
- }
123
- catch {
124
- return null;
125
- }
126
- }
127
- /**
128
- * Helper: Delete keypair from localStorage
129
- */
130
- deleteKeypairFromStorage(username) {
131
- localStorage.removeItem(`rondevu:keypair:${username}`);
132
- }
133
- /**
134
- * Export keypair as JSON string (for backup)
135
- */
136
- exportKeypair(publicKey, privateKey) {
137
- return JSON.stringify({
138
- publicKey,
139
- privateKey,
140
- exportedAt: Date.now()
141
- });
142
- }
143
- /**
144
- * Import keypair from JSON string
145
- */
146
- importKeypair(json) {
147
- const data = JSON.parse(json);
148
- if (!data.publicKey || !data.privateKey) {
149
- throw new Error('Invalid keypair format');
150
- }
151
- return { publicKey: data.publicKey, privateKey: data.privateKey };
152
- }
153
- }