bitcoincash-oauth-client 0.1.1 → 0.2.2

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.
package/dist/index.cjs CHANGED
@@ -9,11 +9,103 @@ Object.defineProperty(exports, '__esModule', { value: true });
9
9
 
10
10
  var libauth = require('@bitauth/libauth');
11
11
 
12
+ /**
13
+ * Custom error classes for Bitcoin Cash OAuth Client
14
+ * Provides specific error types for better error handling
15
+ */
16
+
17
+ /**
18
+ * Base OAuth Error class
19
+ */
20
+ class OAuthError extends Error {
21
+ /**
22
+ * @param {string} message - Error message
23
+ * @param {string} code - Error code
24
+ * @param {number|null} statusCode - HTTP status code (if applicable)
25
+ */
26
+ constructor(message, code, statusCode = null) {
27
+ super(message);
28
+ this.name = 'OAuthError';
29
+ this.code = code;
30
+ this.statusCode = statusCode;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Network-related errors (connection issues, timeouts, etc.)
36
+ */
37
+ class NetworkError extends OAuthError {
38
+ /**
39
+ * @param {string} message - Error message
40
+ * @param {Error|null} originalError - Original error that caused this
41
+ */
42
+ constructor(message, originalError = null) {
43
+ super(message, 'NETWORK_ERROR');
44
+ this.name = 'NetworkError';
45
+ this.originalError = originalError;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Authentication-related errors (invalid credentials, unauthorized access)
51
+ */
52
+ class AuthenticationError extends OAuthError {
53
+ /**
54
+ * @param {string} message - Error message
55
+ * @param {number} statusCode - HTTP status code
56
+ * @param {string} code - Error code
57
+ */
58
+ constructor(message, statusCode, code = 'AUTHENTICATION_ERROR') {
59
+ super(message, code, statusCode);
60
+ this.name = 'AuthenticationError';
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Token has expired
66
+ */
67
+ class TokenExpiredError extends AuthenticationError {
68
+ /**
69
+ * @param {string} [message='Token has expired'] - Error message
70
+ */
71
+ constructor(message = 'Token has expired') {
72
+ super(message, 401, 'TOKEN_EXPIRED');
73
+ this.name = 'TokenExpiredError';
74
+ }
75
+ }
76
+
77
+ /**
78
+ * User not found error
79
+ */
80
+ class UserNotFoundError extends AuthenticationError {
81
+ /**
82
+ * @param {string} [message='User not found'] - Error message
83
+ */
84
+ constructor(message = 'User not found') {
85
+ super(message, 404, 'USER_NOT_FOUND');
86
+ this.name = 'UserNotFoundError';
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Invalid token error
92
+ */
93
+ class InvalidTokenError extends AuthenticationError {
94
+ /**
95
+ * @param {string} [message='Invalid token'] - Error message
96
+ */
97
+ constructor(message = 'Invalid token') {
98
+ super(message, 401, 'INVALID_TOKEN');
99
+ this.name = 'InvalidTokenError';
100
+ }
101
+ }
102
+
12
103
  /**
13
104
  * Environment utilities for cross-platform crypto operations
14
105
  * Handles browser vs Node.js differences
15
106
  */
16
107
 
108
+
17
109
  let cryptoModule = null;
18
110
 
19
111
  /**
@@ -36,6 +128,25 @@ async function getNodeCrypto() {
36
128
  }
37
129
  }
38
130
 
131
+ /**
132
+ * Check if running in Capacitor environment
133
+ * @returns {boolean}
134
+ */
135
+ function isCapacitor() {
136
+ return typeof window !== 'undefined' &&
137
+ window.Capacitor !== undefined &&
138
+ window.Capacitor.isNative === true;
139
+ }
140
+
141
+ /**
142
+ * Check if running in hybrid app environment (Capacitor, Cordova, etc.)
143
+ * @returns {boolean}
144
+ */
145
+ function isHybridApp() {
146
+ return isCapacitor() ||
147
+ (typeof window !== 'undefined' && window.cordova !== undefined);
148
+ }
149
+
39
150
  /**
40
151
  * Generate secure random bytes (cross-platform)
41
152
  * @param {number} length - Number of bytes to generate
@@ -94,15 +205,45 @@ async function ripemd160(data) {
94
205
 
95
206
  /**
96
207
  * Get fetch implementation for current environment
208
+ * @param {Function|null} userProvidedFetch - User-provided fetch implementation
97
209
  * @returns {Function} Fetch implementation
210
+ * @throws {NetworkError} If no fetch implementation is available
98
211
  */
99
- function getFetch() {
212
+ function getFetch(userProvidedFetch = null) {
213
+ // Always prefer user-provided fetch
214
+ if (userProvidedFetch) {
215
+ return userProvidedFetch;
216
+ }
217
+
218
+ // Check if we're in a Capacitor environment
219
+ if (isCapacitor()) {
220
+ throw new NetworkError(
221
+ 'Capacitor environment detected. ' +
222
+ 'Please provide a custom fetch implementation via options.fetch. ' +
223
+ 'Example: new BitcoinCashOAuthClient({ fetch: axiosFetch })'
224
+ );
225
+ }
226
+
227
+ // Check for hybrid app environments (Cordova, etc.)
228
+ if (isHybridApp() && !isCapacitor()) {
229
+ console.warn(
230
+ '[bitcoincash-oauth-client] Hybrid app environment detected. ' +
231
+ 'Consider providing a custom fetch implementation via options.fetch ' +
232
+ 'for better compatibility.'
233
+ );
234
+ }
235
+
100
236
  // Use global fetch if available (browser or Node.js 18+)
101
237
  if (typeof fetch !== 'undefined') {
102
- return fetch;
238
+ // Bind to globalThis to avoid Window issues in some environments
239
+ return fetch.bind(globalThis);
103
240
  }
104
241
 
105
- throw new Error('No fetch implementation available. For Node.js < 18, install node-fetch and pass it as an option.');
242
+ throw new NetworkError(
243
+ 'No fetch implementation available. ' +
244
+ 'For Node.js < 18, install node-fetch and pass it as an option: ' +
245
+ 'new BitcoinCashOAuthClient({ fetch: fetchImplementation })'
246
+ );
106
247
  }
107
248
 
108
249
  /**
@@ -117,7 +258,7 @@ function getFetch() {
117
258
  * @typedef {Object} Keypair
118
259
  * @property {string} privateKey - Hex-encoded private key
119
260
  * @property {string} publicKey - Hex-encoded compressed public key
120
- * @property {string} address - Bitcoin Cash address
261
+ * @property {string} bitcoincash_address - Bitcoin Cash address
121
262
  */
122
263
 
123
264
  /**
@@ -126,6 +267,11 @@ function getFetch() {
126
267
  * @property {string} [network="mainnet"] - Network type ("mainnet" or "testnet")
127
268
  * @property {SecureStorage} [secureStorage] - Storage interface for tokens
128
269
  * @property {Function} [fetch] - Custom fetch implementation (optional)
270
+ * @property {string} [tokenKey="oauth_token"] - Key for storing access token
271
+ * @property {string} [refreshTokenKey="oauth_refresh_token"] - Key for storing refresh token
272
+ * @property {boolean} [autoRefresh=true] - Enable automatic token refresh
273
+ * @property {number} [refreshThreshold=300] - Seconds before expiry to trigger refresh (default: 5 minutes)
274
+ * @property {boolean} [debug=false] - Enable debug logging
129
275
  */
130
276
 
131
277
  /**
@@ -141,6 +287,7 @@ function getFetch() {
141
287
  * @property {string} refresh_token - Refresh token
142
288
  * @property {number} expires_in - Token expiration in seconds
143
289
  * @property {string} token_type - Token type (e.g., "bearer")
290
+ * @property {string[]} [scopes] - Granted scopes
144
291
  */
145
292
 
146
293
  /**
@@ -156,8 +303,41 @@ class BitcoinCashOAuthClient {
156
303
  this.serverUrl = options.serverUrl || "http://localhost:8000";
157
304
  this.network = options.network || "mainnet";
158
305
  this.secureStorage = options.secureStorage || null;
159
- this.fetchImpl = options.fetch || getFetch();
306
+ this.fetchImpl = options.fetch || getFetch(options.fetch);
160
307
  this.secp256k1 = null;
308
+
309
+ // Token storage keys
310
+ this.tokenKey = options.tokenKey || "oauth_token";
311
+ this.refreshTokenKey = options.refreshTokenKey || "oauth_refresh_token";
312
+
313
+ // Auto-refresh settings
314
+ this.autoRefresh = options.autoRefresh !== false; // default true
315
+ this.refreshThreshold = options.refreshThreshold || 300; // 5 minutes before expiry
316
+ this.tokenExpiry = null;
317
+ this.refreshPromise = null;
318
+ this.refreshTimer = null;
319
+
320
+ // Debug mode
321
+ this.debug = options.debug || false;
322
+
323
+ // Store auth params for refresh
324
+ this._authParams = null;
325
+ }
326
+
327
+ /**
328
+ * Log debug messages
329
+ * @private
330
+ * @param {string} message - Message to log
331
+ * @param {*} [data] - Optional data to log
332
+ */
333
+ _log(message, data = null) {
334
+ if (this.debug) {
335
+ if (data !== null) {
336
+ console.log(`[bitcoincash-oauth-client] ${message}`, data);
337
+ } else {
338
+ console.log(`[bitcoincash-oauth-client] ${message}`);
339
+ }
340
+ }
161
341
  }
162
342
 
163
343
  /**
@@ -192,12 +372,14 @@ class BitcoinCashOAuthClient {
192
372
  const publicKeyBytes = this.secp256k1.derivePublicKeyCompressed(privateKeyBytes);
193
373
 
194
374
  // Convert to address
195
- const address = await this.publicKeyToCashAddress(publicKeyBytes);
375
+ const bitcoincash_address = await this.publicKeyToCashAddress(publicKeyBytes);
376
+
377
+ this._log('Generated new keypair', { bitcoincash_address });
196
378
 
197
379
  return {
198
380
  privateKey: this.bytesToHex(privateKeyBytes),
199
381
  publicKey: this.bytesToHex(publicKeyBytes),
200
- address,
382
+ bitcoincash_address,
201
383
  };
202
384
  }
203
385
 
@@ -251,29 +433,62 @@ class BitcoinCashOAuthClient {
251
433
  }
252
434
 
253
435
  /**
254
- * Register a new user with the server
255
- * @param {string} address - Bitcoin Cash address
256
- * @param {string} [userId] - Optional user-provided ID
436
+ * Register a new user with the server (signature required)
437
+ * @param {string} bitcoincash_address - Bitcoin Cash address
438
+ * @param {string} privateKeyHex - Private key for signing (hex-encoded)
439
+ * @param {string} publicKeyHex - Public key for signature verification (hex-encoded)
440
+ * @param {string} userId - User-provided ID (required)
441
+ * @param {number} [timestamp] - Optional Unix timestamp (defaults to now)
442
+ * @param {string} [domain] - Optional domain for message binding
257
443
  * @returns {Promise<Object>} Registration result with assigned userId
444
+ * @throws {NetworkError} If network request fails
445
+ * @throws {AuthenticationError} If registration fails
258
446
  */
259
- async register(address, userId = null) {
260
- const response = await this.fetchImpl(`${this.serverUrl}/auth/register`, {
261
- method: "POST",
262
- headers: {
263
- "Content-Type": "application/json",
264
- },
265
- body: JSON.stringify({
266
- address,
267
- user_id: userId,
268
- }),
269
- });
270
-
271
- if (!response.ok) {
272
- const error = await response.text();
273
- throw new Error(`Registration failed: ${response.status} ${response.statusText} - ${error}`);
447
+ async register(bitcoincash_address, privateKeyHex, publicKeyHex, userId, timestamp = null, domain = null) {
448
+ try {
449
+ const ts = timestamp || Math.floor(Date.now() / 1000);
450
+ const host = domain || this._getDefaultDomain();
451
+ const message = this.createAuthMessage(userId, ts, host);
452
+ const signature = await this.signAuthMessage(message, privateKeyHex);
453
+
454
+ this._log('Registering user', { bitcoincash_address, userId, domain: host });
455
+
456
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/register`, {
457
+ method: "POST",
458
+ headers: {
459
+ "Content-Type": "application/json",
460
+ },
461
+ body: JSON.stringify({
462
+ bitcoincash_address,
463
+ user_id: userId,
464
+ timestamp: ts,
465
+ domain: host,
466
+ public_key: publicKeyHex,
467
+ signature: signature,
468
+ }),
469
+ });
470
+
471
+ if (!response.ok) {
472
+ const errorData = await response.json().catch(() => ({}));
473
+
474
+ if (response.status === 404) {
475
+ throw new UserNotFoundError(errorData.detail || 'User not found');
476
+ }
477
+
478
+ throw new AuthenticationError(
479
+ errorData.detail || `Registration failed: ${response.statusText}`,
480
+ response.status
481
+ );
482
+ }
483
+
484
+ this._log('User registered successfully', { bitcoincash_address, userId });
485
+ return await response.json();
486
+ } catch (error) {
487
+ if (error instanceof OAuthError) {
488
+ throw error;
489
+ }
490
+ throw new NetworkError(`Network error during registration: ${error.message}`, error);
274
491
  }
275
-
276
- return await response.json();
277
492
  }
278
493
 
279
494
  /**
@@ -318,38 +533,196 @@ class BitcoinCashOAuthClient {
318
533
  * @param {number} [timestamp] - Optional timestamp
319
534
  * @param {string} [domain] - Optional domain for message binding
320
535
  * @returns {Promise<AuthenticationResult>} Authentication result with access_token
536
+ * @throws {NetworkError} If network request fails
537
+ * @throws {AuthenticationError} If authentication fails
321
538
  */
322
539
  async authenticate(userId, privateKeyHex, publicKeyHex, timestamp = null, domain = null) {
540
+ this._log('Starting authentication', { userId, domain });
541
+
323
542
  const ts = timestamp || Math.floor(Date.now() / 1000);
324
543
  const message = this.createAuthMessage(userId, ts, domain);
544
+
545
+ this._log('Authentication message created', message);
546
+
325
547
  const signature = await this.signAuthMessage(message, privateKeyHex);
548
+
549
+ this._log('Message signed successfully');
550
+
551
+ try {
552
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/token`, {
553
+ method: "POST",
554
+ headers: {
555
+ "Content-Type": "application/json",
556
+ },
557
+ body: JSON.stringify({
558
+ user_id: userId,
559
+ timestamp: ts,
560
+ public_key: publicKeyHex,
561
+ signature: signature,
562
+ domain: domain || this._getDefaultDomain(), // Include domain in payload
563
+ }),
564
+ });
565
+
566
+ if (!response.ok) {
567
+ const errorData = await response.json().catch(() => ({}));
568
+
569
+ if (response.status === 401) {
570
+ throw new TokenExpiredError(errorData.detail || 'Authentication failed: invalid credentials');
571
+ }
572
+ if (response.status === 404) {
573
+ throw new UserNotFoundError(errorData.detail || 'User not found');
574
+ }
575
+
576
+ throw new AuthenticationError(
577
+ errorData.detail || `Authentication failed: ${response.statusText}`,
578
+ response.status
579
+ );
580
+ }
581
+
582
+ const result = await response.json();
583
+
584
+ this._log('Authentication successful', {
585
+ expires_in: result.expires_in,
586
+ token_type: result.token_type
587
+ });
588
+
589
+ // Store tokens if secure storage is available
590
+ if (this.secureStorage) {
591
+ if (result.access_token) {
592
+ this.secureStorage.setItem(this.tokenKey, result.access_token);
593
+ }
594
+ if (result.refresh_token) {
595
+ this.secureStorage.setItem(this.refreshTokenKey, result.refresh_token);
596
+ }
597
+ }
598
+
599
+ // Track token expiry and schedule refresh
600
+ if (result.expires_in) {
601
+ this.tokenExpiry = Date.now() + (result.expires_in * 1000);
602
+
603
+ // Store auth params for refresh
604
+ this._authParams = { userId, privateKeyHex, publicKeyHex, domain };
605
+
606
+ if (this.autoRefresh) {
607
+ this._scheduleRefresh();
608
+ }
609
+ }
610
+
611
+ return result;
612
+ } catch (error) {
613
+ if (error instanceof OAuthError) {
614
+ throw error;
615
+ }
616
+ throw new NetworkError(`Network error during authentication: ${error.message}`, error);
617
+ }
618
+ }
326
619
 
327
- const response = await this.fetchImpl(`${this.serverUrl}/auth/token`, {
328
- method: "POST",
329
- headers: {
330
- "Content-Type": "application/json",
331
- },
332
- body: JSON.stringify({
333
- user_id: userId,
334
- timestamp: ts,
335
- public_key: publicKeyHex,
336
- signature: signature,
337
- }),
338
- });
339
-
340
- if (!response.ok) {
341
- const error = await response.text();
342
- throw new Error(`Authentication failed: ${response.status} ${response.statusText} - ${error}`);
620
+ /**
621
+ * Get default domain for authentication
622
+ * @private
623
+ * @returns {string}
624
+ */
625
+ _getDefaultDomain() {
626
+ if (typeof window !== 'undefined' && window?.location?.host) {
627
+ return window.location.host;
343
628
  }
629
+ return 'oauth';
630
+ }
344
631
 
345
- const result = await response.json();
632
+ /**
633
+ * Schedule automatic token refresh
634
+ * @private
635
+ */
636
+ _scheduleRefresh() {
637
+ // Clear any existing timer
638
+ if (this.refreshTimer) {
639
+ clearTimeout(this.refreshTimer);
640
+ }
641
+
642
+ const refreshTime = this.tokenExpiry - (this.refreshThreshold * 1000) - Date.now();
346
643
 
347
- // Store token if secure storage is available
348
- if (this.secureStorage && result.access_token) {
349
- this.secureStorage.setItem("oauth_token", result.access_token);
644
+ if (refreshTime > 0) {
645
+ this._log(`Scheduling token refresh in ${Math.floor(refreshTime / 1000)} seconds`);
646
+
647
+ this.refreshTimer = setTimeout(() => {
648
+ this._performRefresh();
649
+ }, refreshTime);
650
+ } else {
651
+ this._log('Token expires too soon, refreshing immediately');
652
+ this._performRefresh();
350
653
  }
654
+ }
351
655
 
352
- return result;
656
+ /**
657
+ * Perform token refresh
658
+ * @private
659
+ */
660
+ async _performRefresh() {
661
+ if (!this._authParams) {
662
+ this._log('No auth params available for refresh');
663
+ return;
664
+ }
665
+
666
+ const { userId, privateKeyHex, publicKeyHex, domain } = this._authParams;
667
+
668
+ try {
669
+ await this.refreshAccessToken(userId, privateKeyHex, publicKeyHex, domain);
670
+ this._log('Token refreshed automatically');
671
+ } catch (error) {
672
+ this._log('Automatic token refresh failed', error.message);
673
+ // Clear stored tokens on refresh failure
674
+ if (this.secureStorage) {
675
+ this.secureStorage.removeItem(this.tokenKey);
676
+ this.secureStorage.removeItem(this.refreshTokenKey);
677
+ }
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Refresh access token with automatic retry on expiration
683
+ * @param {string} userId
684
+ * @param {string} privateKeyHex
685
+ * @param {string} publicKeyHex
686
+ * @param {string} [domain]
687
+ * @returns {Promise<AuthenticationResult>}
688
+ * @throws {AuthenticationError} If refresh fails
689
+ */
690
+ async refreshAccessToken(userId, privateKeyHex, publicKeyHex, domain = null) {
691
+ // Prevent concurrent refresh attempts
692
+ if (this.refreshPromise) {
693
+ return this.refreshPromise;
694
+ }
695
+
696
+ this.refreshPromise = this._doRefresh(userId, privateKeyHex, publicKeyHex, domain);
697
+
698
+ try {
699
+ const result = await this.refreshPromise;
700
+ return result;
701
+ } finally {
702
+ this.refreshPromise = null;
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Internal refresh implementation
708
+ * @private
709
+ */
710
+ async _doRefresh(userId, privateKeyHex, publicKeyHex, domain) {
711
+ this._log('Refreshing access token');
712
+
713
+ // Use refresh token if available, otherwise re-authenticate
714
+ const refreshToken = this.getRefreshToken();
715
+
716
+ if (refreshToken) {
717
+ try {
718
+ return await this.refreshToken(refreshToken);
719
+ } catch (error) {
720
+ this._log('Refresh token failed, falling back to re-authentication');
721
+ }
722
+ }
723
+
724
+ // Fall back to full re-authentication
725
+ return await this.authenticate(userId, privateKeyHex, publicKeyHex, null, domain);
353
726
  }
354
727
 
355
728
  /**
@@ -358,93 +731,251 @@ class BitcoinCashOAuthClient {
358
731
  */
359
732
  getToken() {
360
733
  if (this.secureStorage) {
361
- return this.secureStorage.getItem("oauth_token");
734
+ return this.secureStorage.getItem(this.tokenKey);
362
735
  }
363
736
  return null;
364
737
  }
365
738
 
366
739
  /**
367
- * Make authenticated request
368
- * @param {string} endpoint - API endpoint (relative to serverUrl)
369
- * @param {Object} [options] - Fetch options
370
- * @returns {Promise<Response>}
740
+ * Get stored refresh token
741
+ * @returns {string|null}
742
+ */
743
+ getRefreshToken() {
744
+ if (this.secureStorage) {
745
+ return this.secureStorage.getItem(this.refreshTokenKey);
746
+ }
747
+ return null;
748
+ }
749
+
750
+ /**
751
+ * Validate if the current token is still valid
752
+ * @param {boolean} [serverCheck=false] - If true, validates with the server; if false, only checks local expiry
753
+ * @returns {Promise<boolean>}
371
754
  */
372
- async authenticatedRequest(endpoint, options = {}) {
755
+ async isTokenValid(serverCheck = false) {
373
756
  const token = this.getToken();
374
757
 
375
758
  if (!token) {
376
- throw new Error("No authentication token available");
759
+ this._log('Token validation: no token stored');
760
+ return false;
761
+ }
762
+
763
+ // Local expiry check
764
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
765
+ this._log('Token validation: token expired locally');
766
+ return false;
767
+ }
768
+
769
+ // Server validation (optional)
770
+ if (serverCheck) {
771
+ try {
772
+ this._log('Token validation: checking with server');
773
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/verify`, {
774
+ method: "POST",
775
+ headers: {
776
+ "Authorization": `Bearer ${token}`,
777
+ "Content-Type": "application/json"
778
+ }
779
+ });
780
+
781
+ const isValid = response.ok;
782
+ this._log('Token validation: server check result', isValid);
783
+ return isValid;
784
+ } catch (error) {
785
+ this._log('Token validation: server check failed', error.message);
786
+ return false;
787
+ }
377
788
  }
378
789
 
379
- const headers = {
380
- "Authorization": `Bearer ${token}`,
381
- ...options.headers,
790
+ return true;
791
+ }
792
+
793
+ /**
794
+ * Make authenticated request with automatic retry on token expiration
795
+ * @param {string} endpoint - API endpoint (relative to serverUrl)
796
+ * @param {Object} [options] - Fetch options
797
+ * @param {Object} [authParams] - Parameters to re-authenticate if needed
798
+ * @param {string} authParams.userId - User ID
799
+ * @param {string} authParams.privateKeyHex - Private key in hex
800
+ * @param {string} authParams.publicKeyHex - Public key in hex
801
+ * @param {string} [authParams.domain] - Domain for authentication
802
+ * @returns {Promise<Response>}
803
+ * @throws {AuthenticationError} If no token available and no auth params provided
804
+ */
805
+ async authenticatedRequest(endpoint, options = {}, authParams = null) {
806
+ const makeRequest = async () => {
807
+ const token = this.getToken();
808
+ if (!token) {
809
+ throw new AuthenticationError('No authentication token available', 401, 'NO_TOKEN');
810
+ }
811
+
812
+ const headers = {
813
+ "Authorization": `Bearer ${token}`,
814
+ ...options.headers,
815
+ };
816
+
817
+ return this.fetchImpl(`${this.serverUrl}${endpoint}`, {
818
+ ...options,
819
+ headers,
820
+ });
382
821
  };
383
822
 
384
- return this.fetchImpl(`${this.serverUrl}${endpoint}`, {
385
- ...options,
386
- headers,
387
- });
823
+ let response = await makeRequest();
824
+
825
+ // If token expired and auth params provided, re-authenticate and retry
826
+ if (response.status === 401 && authParams) {
827
+ this._log('Token expired, re-authenticating...');
828
+
829
+ await this.authenticate(
830
+ authParams.userId,
831
+ authParams.privateKeyHex,
832
+ authParams.publicKeyHex,
833
+ null,
834
+ authParams.domain
835
+ );
836
+
837
+ // Retry the request with new token
838
+ response = await makeRequest();
839
+ }
840
+
841
+ return response;
388
842
  }
389
843
 
390
844
  /**
391
- * Refresh access token
845
+ * Refresh access token using refresh token
392
846
  * @param {string} refreshToken
393
847
  * @returns {Promise<AuthenticationResult>}
848
+ * @throws {NetworkError} If network request fails
849
+ * @throws {AuthenticationError} If refresh fails
394
850
  */
395
851
  async refreshToken(refreshToken) {
396
- const response = await this.fetchImpl(`${this.serverUrl}/auth/refresh`, {
397
- method: "POST",
398
- headers: {
399
- "Content-Type": "application/json",
400
- },
401
- body: JSON.stringify({
402
- refresh_token: refreshToken,
403
- }),
404
- });
405
-
406
- if (!response.ok) {
407
- const error = await response.text();
408
- throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${error}`);
852
+ try {
853
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/refresh`, {
854
+ method: "POST",
855
+ headers: {
856
+ "Content-Type": "application/json",
857
+ },
858
+ body: JSON.stringify({
859
+ refresh_token: refreshToken,
860
+ }),
861
+ });
862
+
863
+ if (!response.ok) {
864
+ const errorData = await response.json().catch(() => ({}));
865
+
866
+ if (response.status === 401) {
867
+ throw new InvalidTokenError(errorData.detail || 'Invalid or expired refresh token');
868
+ }
869
+
870
+ throw new AuthenticationError(
871
+ errorData.detail || `Token refresh failed: ${response.statusText}`,
872
+ response.status
873
+ );
874
+ }
875
+
876
+ const result = await response.json();
877
+
878
+ this._log('Token refreshed successfully');
879
+
880
+ // Update stored tokens
881
+ if (this.secureStorage) {
882
+ if (result.access_token) {
883
+ this.secureStorage.setItem(this.tokenKey, result.access_token);
884
+ }
885
+ if (result.refresh_token) {
886
+ this.secureStorage.setItem(this.refreshTokenKey, result.refresh_token);
887
+ }
888
+ }
889
+
890
+ // Update expiry and reschedule refresh
891
+ if (result.expires_in) {
892
+ this.tokenExpiry = Date.now() + (result.expires_in * 1000);
893
+
894
+ if (this.autoRefresh) {
895
+ this._scheduleRefresh();
896
+ }
897
+ }
898
+
899
+ return result;
900
+ } catch (error) {
901
+ if (error instanceof OAuthError) {
902
+ throw error;
903
+ }
904
+ throw new NetworkError(`Network error during token refresh: ${error.message}`, error);
409
905
  }
410
-
411
- const result = await response.json();
412
-
413
- if (this.secureStorage && result.access_token) {
414
- this.secureStorage.setItem("oauth_token", result.access_token);
415
- }
416
-
417
- return result;
418
906
  }
419
907
 
420
908
  /**
421
909
  * Revoke token
422
910
  * @param {string} token
423
911
  * @returns {Promise<Object>}
912
+ * @throws {NetworkError} If network request fails
424
913
  */
425
914
  async revokeToken(token) {
426
- const response = await this.fetchImpl(`${this.serverUrl}/auth/revoke`, {
427
- method: "POST",
428
- headers: {
429
- "Content-Type": "application/json",
430
- },
431
- body: JSON.stringify({
432
- token,
433
- }),
434
- });
435
-
436
- if (!response.ok) {
437
- const error = await response.text();
438
- throw new Error(`Token revocation failed: ${response.status} ${response.statusText} - ${error}`);
915
+ try {
916
+ // Clear any pending refresh
917
+ if (this.refreshTimer) {
918
+ clearTimeout(this.refreshTimer);
919
+ this.refreshTimer = null;
920
+ }
921
+
922
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/revoke`, {
923
+ method: "POST",
924
+ headers: {
925
+ "Content-Type": "application/json",
926
+ },
927
+ body: JSON.stringify({
928
+ token,
929
+ }),
930
+ });
931
+
932
+ if (!response.ok) {
933
+ const errorData = await response.json().catch(() => ({}));
934
+ throw new AuthenticationError(
935
+ errorData.detail || `Token revocation failed: ${response.statusText}`,
936
+ response.status
937
+ );
938
+ }
939
+
940
+ this._log('Token revoked successfully');
941
+
942
+ // Clear stored tokens
943
+ if (this.secureStorage) {
944
+ this.secureStorage.removeItem(this.tokenKey);
945
+ this.secureStorage.removeItem(this.refreshTokenKey);
946
+ }
947
+
948
+ this.tokenExpiry = null;
949
+ this._authParams = null;
950
+
951
+ return await response.json();
952
+ } catch (error) {
953
+ if (error instanceof OAuthError) {
954
+ throw error;
955
+ }
956
+ throw new NetworkError(`Network error during token revocation: ${error.message}`, error);
439
957
  }
958
+ }
440
959
 
441
- if (this.secureStorage) {
442
- this.secureStorage.removeItem("oauth_token");
960
+ /**
961
+ * Clean up resources (clear timers, etc.)
962
+ * Call this when the client is no longer needed
963
+ */
964
+ destroy() {
965
+ if (this.refreshTimer) {
966
+ clearTimeout(this.refreshTimer);
967
+ this.refreshTimer = null;
443
968
  }
444
-
445
- return await response.json();
969
+ this._authParams = null;
970
+ this._log('Client destroyed');
446
971
  }
447
972
  }
448
973
 
974
+ exports.AuthenticationError = AuthenticationError;
449
975
  exports.BitcoinCashOAuthClient = BitcoinCashOAuthClient;
976
+ exports.InvalidTokenError = InvalidTokenError;
977
+ exports.NetworkError = NetworkError;
978
+ exports.OAuthError = OAuthError;
979
+ exports.TokenExpiredError = TokenExpiredError;
980
+ exports.UserNotFoundError = UserNotFoundError;
450
981
  exports.default = BitcoinCashOAuthClient;