bitcoincash-oauth-client 0.1.0 → 0.2.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.
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
  /**
@@ -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
  /**
@@ -194,6 +374,8 @@ class BitcoinCashOAuthClient {
194
374
  // Convert to address
195
375
  const address = await this.publicKeyToCashAddress(publicKeyBytes);
196
376
 
377
+ this._log('Generated new keypair', { address });
378
+
197
379
  return {
198
380
  privateKey: this.bytesToHex(privateKeyBytes),
199
381
  publicKey: this.bytesToHex(publicKeyBytes),
@@ -255,25 +437,43 @@ class BitcoinCashOAuthClient {
255
437
  * @param {string} address - Bitcoin Cash address
256
438
  * @param {string} [userId] - Optional user-provided ID
257
439
  * @returns {Promise<Object>} Registration result with assigned userId
440
+ * @throws {NetworkError} If network request fails
441
+ * @throws {AuthenticationError} If registration fails
258
442
  */
259
443
  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}`);
444
+ try {
445
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/register`, {
446
+ method: "POST",
447
+ headers: {
448
+ "Content-Type": "application/json",
449
+ },
450
+ body: JSON.stringify({
451
+ address,
452
+ user_id: userId,
453
+ }),
454
+ });
455
+
456
+ if (!response.ok) {
457
+ const errorData = await response.json().catch(() => ({}));
458
+
459
+ if (response.status === 404) {
460
+ throw new UserNotFoundError(errorData.detail || 'User not found');
461
+ }
462
+
463
+ throw new AuthenticationError(
464
+ errorData.detail || `Registration failed: ${response.statusText}`,
465
+ response.status
466
+ );
467
+ }
468
+
469
+ this._log('User registered successfully', { address, userId });
470
+ return await response.json();
471
+ } catch (error) {
472
+ if (error instanceof OAuthError) {
473
+ throw error;
474
+ }
475
+ throw new NetworkError(`Network error during registration: ${error.message}`, error);
274
476
  }
275
-
276
- return await response.json();
277
477
  }
278
478
 
279
479
  /**
@@ -318,38 +518,196 @@ class BitcoinCashOAuthClient {
318
518
  * @param {number} [timestamp] - Optional timestamp
319
519
  * @param {string} [domain] - Optional domain for message binding
320
520
  * @returns {Promise<AuthenticationResult>} Authentication result with access_token
521
+ * @throws {NetworkError} If network request fails
522
+ * @throws {AuthenticationError} If authentication fails
321
523
  */
322
524
  async authenticate(userId, privateKeyHex, publicKeyHex, timestamp = null, domain = null) {
525
+ this._log('Starting authentication', { userId, domain });
526
+
323
527
  const ts = timestamp || Math.floor(Date.now() / 1000);
324
528
  const message = this.createAuthMessage(userId, ts, domain);
529
+
530
+ this._log('Authentication message created', message);
531
+
325
532
  const signature = await this.signAuthMessage(message, privateKeyHex);
533
+
534
+ this._log('Message signed successfully');
535
+
536
+ try {
537
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/token`, {
538
+ method: "POST",
539
+ headers: {
540
+ "Content-Type": "application/json",
541
+ },
542
+ body: JSON.stringify({
543
+ user_id: userId,
544
+ timestamp: ts,
545
+ public_key: publicKeyHex,
546
+ signature: signature,
547
+ domain: domain || this._getDefaultDomain(), // Include domain in payload
548
+ }),
549
+ });
550
+
551
+ if (!response.ok) {
552
+ const errorData = await response.json().catch(() => ({}));
553
+
554
+ if (response.status === 401) {
555
+ throw new TokenExpiredError(errorData.detail || 'Authentication failed: invalid credentials');
556
+ }
557
+ if (response.status === 404) {
558
+ throw new UserNotFoundError(errorData.detail || 'User not found');
559
+ }
560
+
561
+ throw new AuthenticationError(
562
+ errorData.detail || `Authentication failed: ${response.statusText}`,
563
+ response.status
564
+ );
565
+ }
566
+
567
+ const result = await response.json();
568
+
569
+ this._log('Authentication successful', {
570
+ expires_in: result.expires_in,
571
+ token_type: result.token_type
572
+ });
573
+
574
+ // Store tokens if secure storage is available
575
+ if (this.secureStorage) {
576
+ if (result.access_token) {
577
+ this.secureStorage.setItem(this.tokenKey, result.access_token);
578
+ }
579
+ if (result.refresh_token) {
580
+ this.secureStorage.setItem(this.refreshTokenKey, result.refresh_token);
581
+ }
582
+ }
583
+
584
+ // Track token expiry and schedule refresh
585
+ if (result.expires_in) {
586
+ this.tokenExpiry = Date.now() + (result.expires_in * 1000);
587
+
588
+ // Store auth params for refresh
589
+ this._authParams = { userId, privateKeyHex, publicKeyHex, domain };
590
+
591
+ if (this.autoRefresh) {
592
+ this._scheduleRefresh();
593
+ }
594
+ }
595
+
596
+ return result;
597
+ } catch (error) {
598
+ if (error instanceof OAuthError) {
599
+ throw error;
600
+ }
601
+ throw new NetworkError(`Network error during authentication: ${error.message}`, error);
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Get default domain for authentication
607
+ * @private
608
+ * @returns {string}
609
+ */
610
+ _getDefaultDomain() {
611
+ if (typeof window !== 'undefined' && window?.location?.host) {
612
+ return window.location.host;
613
+ }
614
+ return 'oauth';
615
+ }
616
+
617
+ /**
618
+ * Schedule automatic token refresh
619
+ * @private
620
+ */
621
+ _scheduleRefresh() {
622
+ // Clear any existing timer
623
+ if (this.refreshTimer) {
624
+ clearTimeout(this.refreshTimer);
625
+ }
626
+
627
+ const refreshTime = this.tokenExpiry - (this.refreshThreshold * 1000) - Date.now();
628
+
629
+ if (refreshTime > 0) {
630
+ this._log(`Scheduling token refresh in ${Math.floor(refreshTime / 1000)} seconds`);
631
+
632
+ this.refreshTimer = setTimeout(() => {
633
+ this._performRefresh();
634
+ }, refreshTime);
635
+ } else {
636
+ this._log('Token expires too soon, refreshing immediately');
637
+ this._performRefresh();
638
+ }
639
+ }
326
640
 
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}`);
641
+ /**
642
+ * Perform token refresh
643
+ * @private
644
+ */
645
+ async _performRefresh() {
646
+ if (!this._authParams) {
647
+ this._log('No auth params available for refresh');
648
+ return;
649
+ }
650
+
651
+ const { userId, privateKeyHex, publicKeyHex, domain } = this._authParams;
652
+
653
+ try {
654
+ await this.refreshAccessToken(userId, privateKeyHex, publicKeyHex, domain);
655
+ this._log('Token refreshed automatically');
656
+ } catch (error) {
657
+ this._log('Automatic token refresh failed', error.message);
658
+ // Clear stored tokens on refresh failure
659
+ if (this.secureStorage) {
660
+ this.secureStorage.removeItem(this.tokenKey);
661
+ this.secureStorage.removeItem(this.refreshTokenKey);
662
+ }
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Refresh access token with automatic retry on expiration
668
+ * @param {string} userId
669
+ * @param {string} privateKeyHex
670
+ * @param {string} publicKeyHex
671
+ * @param {string} [domain]
672
+ * @returns {Promise<AuthenticationResult>}
673
+ * @throws {AuthenticationError} If refresh fails
674
+ */
675
+ async refreshAccessToken(userId, privateKeyHex, publicKeyHex, domain = null) {
676
+ // Prevent concurrent refresh attempts
677
+ if (this.refreshPromise) {
678
+ return this.refreshPromise;
343
679
  }
344
680
 
345
- const result = await response.json();
681
+ this.refreshPromise = this._doRefresh(userId, privateKeyHex, publicKeyHex, domain);
346
682
 
347
- // Store token if secure storage is available
348
- if (this.secureStorage && result.access_token) {
349
- this.secureStorage.setItem("oauth_token", result.access_token);
683
+ try {
684
+ const result = await this.refreshPromise;
685
+ return result;
686
+ } finally {
687
+ this.refreshPromise = null;
350
688
  }
689
+ }
351
690
 
352
- return result;
691
+ /**
692
+ * Internal refresh implementation
693
+ * @private
694
+ */
695
+ async _doRefresh(userId, privateKeyHex, publicKeyHex, domain) {
696
+ this._log('Refreshing access token');
697
+
698
+ // Use refresh token if available, otherwise re-authenticate
699
+ const refreshToken = this.getRefreshToken();
700
+
701
+ if (refreshToken) {
702
+ try {
703
+ return await this.refreshToken(refreshToken);
704
+ } catch (error) {
705
+ this._log('Refresh token failed, falling back to re-authentication');
706
+ }
707
+ }
708
+
709
+ // Fall back to full re-authentication
710
+ return await this.authenticate(userId, privateKeyHex, publicKeyHex, null, domain);
353
711
  }
354
712
 
355
713
  /**
@@ -358,93 +716,251 @@ class BitcoinCashOAuthClient {
358
716
  */
359
717
  getToken() {
360
718
  if (this.secureStorage) {
361
- return this.secureStorage.getItem("oauth_token");
719
+ return this.secureStorage.getItem(this.tokenKey);
362
720
  }
363
721
  return null;
364
722
  }
365
723
 
366
724
  /**
367
- * Make authenticated request
368
- * @param {string} endpoint - API endpoint (relative to serverUrl)
369
- * @param {Object} [options] - Fetch options
370
- * @returns {Promise<Response>}
725
+ * Get stored refresh token
726
+ * @returns {string|null}
727
+ */
728
+ getRefreshToken() {
729
+ if (this.secureStorage) {
730
+ return this.secureStorage.getItem(this.refreshTokenKey);
731
+ }
732
+ return null;
733
+ }
734
+
735
+ /**
736
+ * Validate if the current token is still valid
737
+ * @param {boolean} [serverCheck=false] - If true, validates with the server; if false, only checks local expiry
738
+ * @returns {Promise<boolean>}
371
739
  */
372
- async authenticatedRequest(endpoint, options = {}) {
740
+ async isTokenValid(serverCheck = false) {
373
741
  const token = this.getToken();
374
742
 
375
743
  if (!token) {
376
- throw new Error("No authentication token available");
744
+ this._log('Token validation: no token stored');
745
+ return false;
746
+ }
747
+
748
+ // Local expiry check
749
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
750
+ this._log('Token validation: token expired locally');
751
+ return false;
752
+ }
753
+
754
+ // Server validation (optional)
755
+ if (serverCheck) {
756
+ try {
757
+ this._log('Token validation: checking with server');
758
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/verify`, {
759
+ method: "POST",
760
+ headers: {
761
+ "Authorization": `Bearer ${token}`,
762
+ "Content-Type": "application/json"
763
+ }
764
+ });
765
+
766
+ const isValid = response.ok;
767
+ this._log('Token validation: server check result', isValid);
768
+ return isValid;
769
+ } catch (error) {
770
+ this._log('Token validation: server check failed', error.message);
771
+ return false;
772
+ }
377
773
  }
378
774
 
379
- const headers = {
380
- "Authorization": `Bearer ${token}`,
381
- ...options.headers,
775
+ return true;
776
+ }
777
+
778
+ /**
779
+ * Make authenticated request with automatic retry on token expiration
780
+ * @param {string} endpoint - API endpoint (relative to serverUrl)
781
+ * @param {Object} [options] - Fetch options
782
+ * @param {Object} [authParams] - Parameters to re-authenticate if needed
783
+ * @param {string} authParams.userId - User ID
784
+ * @param {string} authParams.privateKeyHex - Private key in hex
785
+ * @param {string} authParams.publicKeyHex - Public key in hex
786
+ * @param {string} [authParams.domain] - Domain for authentication
787
+ * @returns {Promise<Response>}
788
+ * @throws {AuthenticationError} If no token available and no auth params provided
789
+ */
790
+ async authenticatedRequest(endpoint, options = {}, authParams = null) {
791
+ const makeRequest = async () => {
792
+ const token = this.getToken();
793
+ if (!token) {
794
+ throw new AuthenticationError('No authentication token available', 401, 'NO_TOKEN');
795
+ }
796
+
797
+ const headers = {
798
+ "Authorization": `Bearer ${token}`,
799
+ ...options.headers,
800
+ };
801
+
802
+ return this.fetchImpl(`${this.serverUrl}${endpoint}`, {
803
+ ...options,
804
+ headers,
805
+ });
382
806
  };
383
807
 
384
- return this.fetchImpl(`${this.serverUrl}${endpoint}`, {
385
- ...options,
386
- headers,
387
- });
808
+ let response = await makeRequest();
809
+
810
+ // If token expired and auth params provided, re-authenticate and retry
811
+ if (response.status === 401 && authParams) {
812
+ this._log('Token expired, re-authenticating...');
813
+
814
+ await this.authenticate(
815
+ authParams.userId,
816
+ authParams.privateKeyHex,
817
+ authParams.publicKeyHex,
818
+ null,
819
+ authParams.domain
820
+ );
821
+
822
+ // Retry the request with new token
823
+ response = await makeRequest();
824
+ }
825
+
826
+ return response;
388
827
  }
389
828
 
390
829
  /**
391
- * Refresh access token
830
+ * Refresh access token using refresh token
392
831
  * @param {string} refreshToken
393
832
  * @returns {Promise<AuthenticationResult>}
833
+ * @throws {NetworkError} If network request fails
834
+ * @throws {AuthenticationError} If refresh fails
394
835
  */
395
836
  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}`);
837
+ try {
838
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/refresh`, {
839
+ method: "POST",
840
+ headers: {
841
+ "Content-Type": "application/json",
842
+ },
843
+ body: JSON.stringify({
844
+ refresh_token: refreshToken,
845
+ }),
846
+ });
847
+
848
+ if (!response.ok) {
849
+ const errorData = await response.json().catch(() => ({}));
850
+
851
+ if (response.status === 401) {
852
+ throw new InvalidTokenError(errorData.detail || 'Invalid or expired refresh token');
853
+ }
854
+
855
+ throw new AuthenticationError(
856
+ errorData.detail || `Token refresh failed: ${response.statusText}`,
857
+ response.status
858
+ );
859
+ }
860
+
861
+ const result = await response.json();
862
+
863
+ this._log('Token refreshed successfully');
864
+
865
+ // Update stored tokens
866
+ if (this.secureStorage) {
867
+ if (result.access_token) {
868
+ this.secureStorage.setItem(this.tokenKey, result.access_token);
869
+ }
870
+ if (result.refresh_token) {
871
+ this.secureStorage.setItem(this.refreshTokenKey, result.refresh_token);
872
+ }
873
+ }
874
+
875
+ // Update expiry and reschedule refresh
876
+ if (result.expires_in) {
877
+ this.tokenExpiry = Date.now() + (result.expires_in * 1000);
878
+
879
+ if (this.autoRefresh) {
880
+ this._scheduleRefresh();
881
+ }
882
+ }
883
+
884
+ return result;
885
+ } catch (error) {
886
+ if (error instanceof OAuthError) {
887
+ throw error;
888
+ }
889
+ throw new NetworkError(`Network error during token refresh: ${error.message}`, error);
409
890
  }
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
891
  }
419
892
 
420
893
  /**
421
894
  * Revoke token
422
895
  * @param {string} token
423
896
  * @returns {Promise<Object>}
897
+ * @throws {NetworkError} If network request fails
424
898
  */
425
899
  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}`);
900
+ try {
901
+ // Clear any pending refresh
902
+ if (this.refreshTimer) {
903
+ clearTimeout(this.refreshTimer);
904
+ this.refreshTimer = null;
905
+ }
906
+
907
+ const response = await this.fetchImpl(`${this.serverUrl}/auth/revoke`, {
908
+ method: "POST",
909
+ headers: {
910
+ "Content-Type": "application/json",
911
+ },
912
+ body: JSON.stringify({
913
+ token,
914
+ }),
915
+ });
916
+
917
+ if (!response.ok) {
918
+ const errorData = await response.json().catch(() => ({}));
919
+ throw new AuthenticationError(
920
+ errorData.detail || `Token revocation failed: ${response.statusText}`,
921
+ response.status
922
+ );
923
+ }
924
+
925
+ this._log('Token revoked successfully');
926
+
927
+ // Clear stored tokens
928
+ if (this.secureStorage) {
929
+ this.secureStorage.removeItem(this.tokenKey);
930
+ this.secureStorage.removeItem(this.refreshTokenKey);
931
+ }
932
+
933
+ this.tokenExpiry = null;
934
+ this._authParams = null;
935
+
936
+ return await response.json();
937
+ } catch (error) {
938
+ if (error instanceof OAuthError) {
939
+ throw error;
940
+ }
941
+ throw new NetworkError(`Network error during token revocation: ${error.message}`, error);
439
942
  }
943
+ }
440
944
 
441
- if (this.secureStorage) {
442
- this.secureStorage.removeItem("oauth_token");
945
+ /**
946
+ * Clean up resources (clear timers, etc.)
947
+ * Call this when the client is no longer needed
948
+ */
949
+ destroy() {
950
+ if (this.refreshTimer) {
951
+ clearTimeout(this.refreshTimer);
952
+ this.refreshTimer = null;
443
953
  }
444
-
445
- return await response.json();
954
+ this._authParams = null;
955
+ this._log('Client destroyed');
446
956
  }
447
957
  }
448
958
 
959
+ exports.AuthenticationError = AuthenticationError;
449
960
  exports.BitcoinCashOAuthClient = BitcoinCashOAuthClient;
961
+ exports.InvalidTokenError = InvalidTokenError;
962
+ exports.NetworkError = NetworkError;
963
+ exports.OAuthError = OAuthError;
964
+ exports.TokenExpiredError = TokenExpiredError;
965
+ exports.UserNotFoundError = UserNotFoundError;
450
966
  exports.default = BitcoinCashOAuthClient;