@xtr-dev/rondevu-client 0.7.12 → 0.8.1

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,339 @@
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.status = {
18
+ activeOffers: 0,
19
+ activeConnections: 0,
20
+ totalConnectionsHandled: 0,
21
+ failedOfferCreations: 0
22
+ };
23
+ this.offersApi = new RondevuOffers(baseUrl, credentials);
24
+ this.usernameApi = new RondevuUsername(baseUrl);
25
+ }
26
+ /**
27
+ * Start the pooled service
28
+ */
29
+ async start() {
30
+ const poolSize = this.options.poolSize || 1;
31
+ // 1. Create initial service (publishes first offer)
32
+ const service = await this.publishInitialService();
33
+ this.serviceId = service.serviceId;
34
+ this.uuid = service.uuid;
35
+ // 2. Create additional offers for pool (poolSize - 1)
36
+ const additionalOffers = [];
37
+ if (poolSize > 1) {
38
+ try {
39
+ const offers = await this.createOffers(poolSize - 1);
40
+ additionalOffers.push(...offers);
41
+ }
42
+ catch (error) {
43
+ this.handleError(error, 'initial-offer-creation');
44
+ }
45
+ }
46
+ // 3. Initialize OfferPool with all offers
47
+ this.offerPool = new OfferPool(this.offersApi, {
48
+ poolSize,
49
+ pollingInterval: this.options.pollingInterval || 2000,
50
+ onAnswered: (answer) => this.handleConnection(answer),
51
+ onRefill: (count) => this.createOffers(count),
52
+ onError: (err, ctx) => this.handleError(err, ctx)
53
+ });
54
+ // Add all offers to pool
55
+ const allOffers = [
56
+ { id: service.offerId, peerId: this.credentials.peerId, sdp: '', topics: [], expiresAt: service.expiresAt, lastSeen: Date.now() },
57
+ ...additionalOffers
58
+ ];
59
+ await this.offerPool.addOffers(allOffers);
60
+ // 4. Start polling
61
+ await this.offerPool.start();
62
+ // Update status
63
+ this.updateStatus();
64
+ // 5. Return handle
65
+ return {
66
+ serviceId: this.serviceId,
67
+ uuid: this.uuid,
68
+ offerId: service.offerId,
69
+ unpublish: () => this.stop(),
70
+ getStatus: () => this.getStatus(),
71
+ addOffers: (count) => this.manualRefill(count)
72
+ };
73
+ }
74
+ /**
75
+ * Stop the pooled service and clean up
76
+ */
77
+ async stop() {
78
+ // 1. Stop accepting new connections
79
+ if (this.offerPool) {
80
+ await this.offerPool.stop();
81
+ }
82
+ // 2. Delete remaining offers
83
+ if (this.offerPool) {
84
+ const offerIds = this.offerPool.getActiveOfferIds();
85
+ await Promise.allSettled(offerIds.map(id => this.offersApi.delete(id).catch(() => { })));
86
+ }
87
+ // 3. Close active connections
88
+ const closePromises = Array.from(this.connections.values()).map(async (conn) => {
89
+ try {
90
+ // Give a brief moment for graceful closure
91
+ await new Promise(resolve => setTimeout(resolve, 100));
92
+ conn.peer.pc.close();
93
+ }
94
+ catch {
95
+ // Ignore errors during cleanup
96
+ }
97
+ });
98
+ await Promise.allSettled(closePromises);
99
+ // 4. Delete service if we have a serviceId
100
+ if (this.serviceId) {
101
+ try {
102
+ const response = await fetch(`${this.baseUrl}/services/${this.serviceId}`, {
103
+ method: 'DELETE',
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
107
+ },
108
+ body: JSON.stringify({ username: this.options.username })
109
+ });
110
+ if (!response.ok) {
111
+ console.error('Failed to delete service:', await response.text());
112
+ }
113
+ }
114
+ catch (error) {
115
+ console.error('Error deleting service:', error);
116
+ }
117
+ }
118
+ // Clear all state
119
+ this.connections.clear();
120
+ this.offerPool = undefined;
121
+ }
122
+ /**
123
+ * Handle an answered offer by setting up the connection
124
+ */
125
+ async handleConnection(answer) {
126
+ const connectionId = this.generateConnectionId();
127
+ try {
128
+ // Create peer connection
129
+ const peer = new RondevuPeer(this.offersApi, this.options.rtcConfig || {
130
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
131
+ });
132
+ peer.role = 'offerer';
133
+ peer.offerId = answer.offerId;
134
+ // Set remote description (the answer)
135
+ await peer.pc.setRemoteDescription({
136
+ type: 'answer',
137
+ sdp: answer.sdp
138
+ });
139
+ // Wait for data channel (answerer creates it, we receive it)
140
+ const channel = await new Promise((resolve, reject) => {
141
+ const timeout = setTimeout(() => reject(new Error('Timeout waiting for data channel')), 30000);
142
+ peer.on('datachannel', (ch) => {
143
+ clearTimeout(timeout);
144
+ resolve(ch);
145
+ });
146
+ // Also check if channel already exists
147
+ if (peer.pc.ondatachannel) {
148
+ const existingHandler = peer.pc.ondatachannel;
149
+ peer.pc.ondatachannel = (event) => {
150
+ clearTimeout(timeout);
151
+ resolve(event.channel);
152
+ if (existingHandler)
153
+ existingHandler.call(peer.pc, event);
154
+ };
155
+ }
156
+ else {
157
+ peer.pc.ondatachannel = (event) => {
158
+ clearTimeout(timeout);
159
+ resolve(event.channel);
160
+ };
161
+ }
162
+ });
163
+ // Register connection
164
+ this.connections.set(connectionId, {
165
+ peer,
166
+ channel,
167
+ connectedAt: Date.now(),
168
+ offerId: answer.offerId
169
+ });
170
+ this.status.activeConnections++;
171
+ this.status.totalConnectionsHandled++;
172
+ // Setup cleanup on disconnect
173
+ peer.on('disconnected', () => {
174
+ this.connections.delete(connectionId);
175
+ this.status.activeConnections--;
176
+ this.updateStatus();
177
+ });
178
+ peer.on('failed', () => {
179
+ this.connections.delete(connectionId);
180
+ this.status.activeConnections--;
181
+ this.updateStatus();
182
+ });
183
+ // Update status
184
+ this.updateStatus();
185
+ // Invoke user handler (wrapped in try-catch)
186
+ try {
187
+ this.options.handler(channel, peer, connectionId);
188
+ }
189
+ catch (handlerError) {
190
+ this.handleError(handlerError, 'handler');
191
+ }
192
+ }
193
+ catch (error) {
194
+ this.handleError(error, 'connection-setup');
195
+ }
196
+ }
197
+ /**
198
+ * Create multiple offers
199
+ */
200
+ async createOffers(count) {
201
+ if (count <= 0) {
202
+ return [];
203
+ }
204
+ // Server supports max 10 offers per request
205
+ const batchSize = Math.min(count, 10);
206
+ const offers = [];
207
+ try {
208
+ // Create peer connections and generate offers
209
+ const offerRequests = [];
210
+ for (let i = 0; i < batchSize; i++) {
211
+ const pc = new RTCPeerConnection(this.options.rtcConfig || {
212
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
213
+ });
214
+ // Create data channel (required for offers)
215
+ pc.createDataChannel('rondevu-service');
216
+ // Create offer
217
+ const offer = await pc.createOffer();
218
+ await pc.setLocalDescription(offer);
219
+ if (!offer.sdp) {
220
+ pc.close();
221
+ throw new Error('Failed to generate SDP');
222
+ }
223
+ offerRequests.push({
224
+ sdp: offer.sdp,
225
+ topics: [], // V2 doesn't use topics
226
+ ttl: this.options.ttl
227
+ });
228
+ // Close the PC immediately - we only needed the SDP
229
+ pc.close();
230
+ }
231
+ // Batch create offers
232
+ const createdOffers = await this.offersApi.create(offerRequests);
233
+ offers.push(...createdOffers);
234
+ }
235
+ catch (error) {
236
+ this.status.failedOfferCreations++;
237
+ this.handleError(error, 'offer-creation');
238
+ throw error;
239
+ }
240
+ return offers;
241
+ }
242
+ /**
243
+ * Publish the initial service (creates first offer)
244
+ */
245
+ async publishInitialService() {
246
+ const { username, privateKey, serviceFqn, rtcConfig, isPublic, metadata, ttl } = this.options;
247
+ // Create peer connection for initial offer
248
+ const pc = new RTCPeerConnection(rtcConfig || {
249
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
250
+ });
251
+ pc.createDataChannel('rondevu-service');
252
+ // Create offer
253
+ const offer = await pc.createOffer();
254
+ await pc.setLocalDescription(offer);
255
+ if (!offer.sdp) {
256
+ pc.close();
257
+ throw new Error('Failed to generate SDP');
258
+ }
259
+ // Create signature
260
+ const timestamp = Date.now();
261
+ const message = `publish:${username}:${serviceFqn}:${timestamp}`;
262
+ const signature = await this.usernameApi.signMessage(message, privateKey);
263
+ // Publish service
264
+ const response = await fetch(`${this.baseUrl}/services`, {
265
+ method: 'POST',
266
+ headers: {
267
+ 'Content-Type': 'application/json',
268
+ 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}`
269
+ },
270
+ body: JSON.stringify({
271
+ username,
272
+ serviceFqn,
273
+ sdp: offer.sdp,
274
+ ttl,
275
+ isPublic,
276
+ metadata,
277
+ signature,
278
+ message
279
+ })
280
+ });
281
+ pc.close();
282
+ if (!response.ok) {
283
+ const error = await response.json();
284
+ throw new Error(error.error || 'Failed to publish service');
285
+ }
286
+ const data = await response.json();
287
+ return {
288
+ serviceId: data.serviceId,
289
+ uuid: data.uuid,
290
+ offerId: data.offerId,
291
+ expiresAt: data.expiresAt
292
+ };
293
+ }
294
+ /**
295
+ * Manually add offers to the pool
296
+ */
297
+ async manualRefill(count) {
298
+ if (!this.offerPool) {
299
+ throw new Error('Pool not started');
300
+ }
301
+ const offers = await this.createOffers(count);
302
+ await this.offerPool.addOffers(offers);
303
+ this.updateStatus();
304
+ }
305
+ /**
306
+ * Get current pool status
307
+ */
308
+ getStatus() {
309
+ return { ...this.status };
310
+ }
311
+ /**
312
+ * Update status and notify listeners
313
+ */
314
+ updateStatus() {
315
+ if (this.offerPool) {
316
+ this.status.activeOffers = this.offerPool.getActiveOfferCount();
317
+ }
318
+ if (this.options.onPoolStatus) {
319
+ this.options.onPoolStatus(this.getStatus());
320
+ }
321
+ }
322
+ /**
323
+ * Handle errors
324
+ */
325
+ handleError(error, context) {
326
+ if (this.options.onError) {
327
+ this.options.onError(error, context);
328
+ }
329
+ else {
330
+ console.error(`ServicePool error (${context}):`, error);
331
+ }
332
+ }
333
+ /**
334
+ * Generate a unique connection ID
335
+ */
336
+ generateConnectionId() {
337
+ return `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
338
+ }
339
+ }
@@ -0,0 +1,79 @@
1
+ import RondevuPeer from './peer/index.js';
2
+ import { PooledServiceHandle, PoolStatus } from './service-pool.js';
3
+ /**
4
+ * Service publish result
5
+ */
6
+ export interface ServicePublishResult {
7
+ serviceId: string;
8
+ uuid: string;
9
+ offerId: string;
10
+ expiresAt: number;
11
+ }
12
+ /**
13
+ * Service publish options
14
+ */
15
+ export interface PublishServiceOptions {
16
+ username: string;
17
+ privateKey: string;
18
+ serviceFqn: string;
19
+ rtcConfig?: RTCConfiguration;
20
+ isPublic?: boolean;
21
+ metadata?: Record<string, any>;
22
+ ttl?: number;
23
+ onConnection?: (peer: RondevuPeer) => void;
24
+ }
25
+ /**
26
+ * Service handle for managing an exposed service
27
+ */
28
+ export interface ServiceHandle {
29
+ serviceId: string;
30
+ uuid: string;
31
+ offerId: string;
32
+ unpublish: () => Promise<void>;
33
+ }
34
+ /**
35
+ * Rondevu Services API
36
+ * Handles service publishing and management
37
+ */
38
+ export declare class RondevuServices {
39
+ private baseUrl;
40
+ private credentials;
41
+ private usernameApi;
42
+ private offersApi;
43
+ constructor(baseUrl: string, credentials: {
44
+ peerId: string;
45
+ secret: string;
46
+ });
47
+ /**
48
+ * Publishes a service
49
+ */
50
+ publishService(options: PublishServiceOptions): Promise<ServicePublishResult>;
51
+ /**
52
+ * Unpublishes a service
53
+ */
54
+ unpublishService(serviceId: string, username: string): Promise<void>;
55
+ /**
56
+ * Exposes a service with an automatic connection handler
57
+ * This is a convenience method that publishes the service and manages connections
58
+ *
59
+ * Set poolSize > 1 to enable offer pooling for handling multiple concurrent connections
60
+ */
61
+ exposeService(options: Omit<PublishServiceOptions, 'onConnection'> & {
62
+ handler: (channel: RTCDataChannel, peer: RondevuPeer, connectionId?: string) => void;
63
+ poolSize?: number;
64
+ pollingInterval?: number;
65
+ onPoolStatus?: (status: PoolStatus) => void;
66
+ onError?: (error: Error, context: string) => void;
67
+ }): Promise<ServiceHandle | PooledServiceHandle>;
68
+ /**
69
+ * Validates service FQN format
70
+ */
71
+ private validateServiceFqn;
72
+ /**
73
+ * Parses a service FQN into name and version
74
+ */
75
+ parseServiceFqn(fqn: string): {
76
+ name: string;
77
+ version: string;
78
+ };
79
+ }
@@ -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
+ }