@xtr-dev/rondevu-client 0.9.2 → 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 -488
  72. package/dist/usernames.d.ts +0 -79
  73. package/dist/usernames.js +0 -153
@@ -1,488 +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
- const pendingCandidates = []; // Store candidates before we have offer IDs
249
- for (let i = 0; i < batchSize; i++) {
250
- const pc = new RTCPeerConnection(this.options.rtcConfig || {
251
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
252
- });
253
- // Create data channel (required for offers) and save reference
254
- const channel = pc.createDataChannel('rondevu-service');
255
- dataChannels.push(channel);
256
- // Set up temporary candidate collector BEFORE setLocalDescription
257
- const candidatesForThisOffer = [];
258
- pendingCandidates.push(candidatesForThisOffer);
259
- pc.onicecandidate = (event) => {
260
- if (event.candidate) {
261
- const candidateData = event.candidate.toJSON();
262
- if (candidateData.candidate && candidateData.candidate !== '') {
263
- const type = candidateData.candidate.includes('typ host') ? 'host' :
264
- candidateData.candidate.includes('typ srflx') ? 'srflx' :
265
- candidateData.candidate.includes('typ relay') ? 'relay' : 'unknown';
266
- console.log(`🧊 Service pool generated ${type} ICE candidate:`, candidateData.candidate);
267
- candidatesForThisOffer.push(candidateData);
268
- }
269
- }
270
- else {
271
- console.log('🧊 Service pool ICE gathering complete');
272
- }
273
- };
274
- // Create offer
275
- const offer = await pc.createOffer();
276
- await pc.setLocalDescription(offer); // ICE gathering starts here, candidates go to collector
277
- if (!offer.sdp) {
278
- pc.close();
279
- throw new Error('Failed to generate SDP');
280
- }
281
- offerRequests.push({
282
- sdp: offer.sdp,
283
- topics: [], // V2 doesn't use topics
284
- ttl: this.options.ttl
285
- });
286
- // Keep peer connection alive - DO NOT CLOSE
287
- peerConnections.push(pc);
288
- }
289
- // Batch create offers
290
- const createdOffers = await this.offersApi.create(offerRequests);
291
- offers.push(...createdOffers);
292
- // Now send all pending candidates and set up handlers for future ones
293
- for (let i = 0; i < peerConnections.length; i++) {
294
- const pc = peerConnections[i];
295
- const offerId = createdOffers[i].id;
296
- const candidates = pendingCandidates[i];
297
- // Send any candidates that were collected while waiting for offer ID
298
- if (candidates.length > 0) {
299
- console.log(`📤 Sending ${candidates.length} pending ICE candidate(s) for offer ${offerId}`);
300
- try {
301
- await this.offersApi.addIceCandidates(offerId, candidates);
302
- console.log(`✅ Sent ${candidates.length} pending ICE candidate(s)`);
303
- }
304
- catch (err) {
305
- console.error('❌ Error sending pending ICE candidates:', err);
306
- }
307
- }
308
- // Replace temporary handler with permanent one for any future candidates
309
- pc.onicecandidate = async (event) => {
310
- if (event.candidate) {
311
- const candidateData = event.candidate.toJSON();
312
- if (candidateData.candidate && candidateData.candidate !== '') {
313
- const type = candidateData.candidate.includes('typ host') ? 'host' :
314
- candidateData.candidate.includes('typ srflx') ? 'srflx' :
315
- candidateData.candidate.includes('typ relay') ? 'relay' : 'unknown';
316
- console.log(`🧊 Service pool generated late ${type} ICE candidate:`, candidateData.candidate);
317
- try {
318
- await this.offersApi.addIceCandidates(offerId, [candidateData]);
319
- console.log(`✅ Sent late ${type} ICE candidate`);
320
- }
321
- catch (err) {
322
- console.error(`❌ Error sending ${type} ICE candidate:`, err);
323
- }
324
- }
325
- }
326
- };
327
- }
328
- }
329
- catch (error) {
330
- // Close any created peer connections on error
331
- peerConnections.forEach(pc => pc.close());
332
- this.status.failedOfferCreations++;
333
- this.handleError(error, 'offer-creation');
334
- throw error;
335
- }
336
- return { offers, peerConnections, dataChannels };
337
- }
338
- /**
339
- * Publish the initial service (creates first offer)
340
- */
341
- async publishInitialService() {
342
- const { username, privateKey, serviceFqn, rtcConfig, isPublic, metadata, ttl } = this.options;
343
- // Create peer connection for initial offer
344
- const pc = new RTCPeerConnection(rtcConfig || {
345
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
346
- });
347
- const dataChannel = pc.createDataChannel('rondevu-service');
348
- // Collect candidates before we have offer ID
349
- const pendingCandidates = [];
350
- // Set up temporary candidate collector BEFORE setLocalDescription
351
- pc.onicecandidate = (event) => {
352
- if (event.candidate) {
353
- const candidateData = event.candidate.toJSON();
354
- if (candidateData.candidate && candidateData.candidate !== '') {
355
- const type = candidateData.candidate.includes('typ host') ? 'host' :
356
- candidateData.candidate.includes('typ srflx') ? 'srflx' :
357
- candidateData.candidate.includes('typ relay') ? 'relay' : 'unknown';
358
- console.log(`🧊 Initial service generated ${type} ICE candidate:`, candidateData.candidate);
359
- pendingCandidates.push(candidateData);
360
- }
361
- }
362
- else {
363
- console.log('🧊 Initial service ICE gathering complete');
364
- }
365
- };
366
- // Create offer
367
- const offer = await pc.createOffer();
368
- await pc.setLocalDescription(offer); // ICE gathering starts here
369
- if (!offer.sdp) {
370
- pc.close();
371
- throw new Error('Failed to generate SDP');
372
- }
373
- // Store the SDP
374
- const offerSdp = offer.sdp;
375
- // Create signature
376
- const timestamp = Date.now();
377
- const message = `publish:${username}:${serviceFqn}:${timestamp}`;
378
- const signature = await this.usernameApi.signMessage(message, privateKey);
379
- // Publish service
380
- const response = await fetch(`${this.baseUrl}/services`, {
381
- method: 'POST',
382
- headers: {
383
- 'Content-Type': 'application/json',
384
- 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
385
- },
386
- body: JSON.stringify({
387
- username,
388
- serviceFqn,
389
- sdp: offerSdp,
390
- ttl,
391
- isPublic,
392
- metadata,
393
- signature,
394
- message
395
- })
396
- });
397
- if (!response.ok) {
398
- pc.close();
399
- const error = await response.json();
400
- throw new Error(error.error || 'Failed to publish service');
401
- }
402
- const data = await response.json();
403
- // Send any pending candidates
404
- if (pendingCandidates.length > 0) {
405
- console.log(`📤 Sending ${pendingCandidates.length} pending ICE candidate(s) for initial service`);
406
- try {
407
- await this.offersApi.addIceCandidates(data.offerId, pendingCandidates);
408
- console.log(`✅ Sent ${pendingCandidates.length} pending ICE candidate(s)`);
409
- }
410
- catch (err) {
411
- console.error('❌ Error sending pending ICE candidates:', err);
412
- }
413
- }
414
- // Set up handler for any future candidates
415
- pc.onicecandidate = async (event) => {
416
- if (event.candidate) {
417
- const candidateData = event.candidate.toJSON();
418
- if (candidateData.candidate && candidateData.candidate !== '') {
419
- const type = candidateData.candidate.includes('typ host') ? 'host' :
420
- candidateData.candidate.includes('typ srflx') ? 'srflx' :
421
- candidateData.candidate.includes('typ relay') ? 'relay' : 'unknown';
422
- console.log(`🧊 Initial service generated late ${type} ICE candidate:`, candidateData.candidate);
423
- try {
424
- await this.offersApi.addIceCandidates(data.offerId, [candidateData]);
425
- console.log(`✅ Sent late ${type} ICE candidate`);
426
- }
427
- catch (err) {
428
- console.error(`❌ Error sending ${type} ICE candidate:`, err);
429
- }
430
- }
431
- }
432
- };
433
- return {
434
- serviceId: data.serviceId,
435
- uuid: data.uuid,
436
- offerId: data.offerId,
437
- offerSdp,
438
- expiresAt: data.expiresAt,
439
- peerConnection: pc, // Keep peer connection alive
440
- dataChannel // Keep data channel alive
441
- };
442
- }
443
- /**
444
- * Manually add offers to the pool
445
- */
446
- async manualRefill(count) {
447
- if (!this.offerPool) {
448
- throw new Error('Pool not started');
449
- }
450
- const result = await this.createOffers(count);
451
- await this.offerPool.addOffers(result.offers, result.peerConnections, result.dataChannels);
452
- this.updateStatus();
453
- }
454
- /**
455
- * Get current pool status
456
- */
457
- getStatus() {
458
- return { ...this.status };
459
- }
460
- /**
461
- * Update status and notify listeners
462
- */
463
- updateStatus() {
464
- if (this.offerPool) {
465
- this.status.activeOffers = this.offerPool.getActiveOfferCount();
466
- }
467
- if (this.options.onPoolStatus) {
468
- this.options.onPoolStatus(this.getStatus());
469
- }
470
- }
471
- /**
472
- * Handle errors
473
- */
474
- handleError(error, context) {
475
- if (this.options.onError) {
476
- this.options.onError(error, context);
477
- }
478
- else {
479
- console.error(`ServicePool error (${context}):`, error);
480
- }
481
- }
482
- /**
483
- * Generate a unique connection ID
484
- */
485
- generateConnectionId() {
486
- return `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
487
- }
488
- }
@@ -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
- }