bitcoincash-oauth-client 0.1.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 ADDED
@@ -0,0 +1,450 @@
1
+ /**
2
+ * bitcoincash-oauth-client v1.0.0
3
+ * Universal Bitcoin Cash OAuth Client
4
+ * Works in both browser and Node.js environments
5
+ */
6
+ 'use strict';
7
+
8
+ Object.defineProperty(exports, '__esModule', { value: true });
9
+
10
+ var libauth = require('@bitauth/libauth');
11
+
12
+ /**
13
+ * Environment utilities for cross-platform crypto operations
14
+ * Handles browser vs Node.js differences
15
+ */
16
+
17
+ let cryptoModule = null;
18
+
19
+ /**
20
+ * Get the Node.js crypto module (cached)
21
+ * @returns {Object|null} Crypto module or null if not available
22
+ */
23
+ async function getNodeCrypto() {
24
+ if (cryptoModule !== null) {
25
+ return cryptoModule;
26
+ }
27
+
28
+ try {
29
+ // Dynamic import for ES module compatibility
30
+ const crypto = await import('crypto');
31
+ cryptoModule = crypto.default || crypto;
32
+ return cryptoModule;
33
+ } catch (e) {
34
+ cryptoModule = false;
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Generate secure random bytes (cross-platform)
41
+ * @param {number} length - Number of bytes to generate
42
+ * @returns {Uint8Array} Random bytes
43
+ */
44
+ async function getRandomBytes(length) {
45
+ // Browser environment: use crypto.getRandomValues
46
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
47
+ return crypto.getRandomValues(new Uint8Array(length));
48
+ }
49
+
50
+ // Node.js environment: use crypto module
51
+ const nodeCrypto = await getNodeCrypto();
52
+ if (nodeCrypto) {
53
+ return new Uint8Array(nodeCrypto.randomBytes(length));
54
+ }
55
+
56
+ throw new Error('Unable to generate secure random bytes - no crypto implementation available');
57
+ }
58
+
59
+ /**
60
+ * SHA256 hash (cross-platform)
61
+ * @param {Uint8Array} data
62
+ * @returns {Promise<Uint8Array>}
63
+ */
64
+ async function sha256(data) {
65
+ // Browser: use crypto.subtle
66
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
67
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
68
+ return new Uint8Array(hashBuffer);
69
+ }
70
+
71
+ // Node.js: use crypto module
72
+ const nodeCrypto = await getNodeCrypto();
73
+ if (nodeCrypto) {
74
+ return new Uint8Array(nodeCrypto.createHash('sha256').update(data).digest());
75
+ }
76
+
77
+ throw new Error('SHA256 not available');
78
+ }
79
+
80
+ /**
81
+ * RIPEMD160 hash (cross-platform)
82
+ * @param {Uint8Array} data
83
+ * @returns {Promise<Uint8Array>}
84
+ */
85
+ async function ripemd160(data) {
86
+ // Node.js: use crypto module (preferred)
87
+ const nodeCrypto = await getNodeCrypto();
88
+ if (nodeCrypto) {
89
+ return new Uint8Array(nodeCrypto.createHash('ripemd160').update(data).digest());
90
+ }
91
+
92
+ throw new Error('RIPEMD160 not available. This is required for Bitcoin Cash address generation.');
93
+ }
94
+
95
+ /**
96
+ * Get fetch implementation for current environment
97
+ * @returns {Function} Fetch implementation
98
+ */
99
+ function getFetch() {
100
+ // Use global fetch if available (browser or Node.js 18+)
101
+ if (typeof fetch !== 'undefined') {
102
+ return fetch;
103
+ }
104
+
105
+ throw new Error('No fetch implementation available. For Node.js < 18, install node-fetch and pass it as an option.');
106
+ }
107
+
108
+ /**
109
+ * Bitcoin Cash OAuth Client Library
110
+ * Universal client that works in both browser and Node.js environments
111
+ *
112
+ * Uses libauth for key generation and ECDSA signing
113
+ */
114
+
115
+
116
+ /**
117
+ * @typedef {Object} Keypair
118
+ * @property {string} privateKey - Hex-encoded private key
119
+ * @property {string} publicKey - Hex-encoded compressed public key
120
+ * @property {string} address - Bitcoin Cash address
121
+ */
122
+
123
+ /**
124
+ * @typedef {Object} OAuthClientOptions
125
+ * @property {string} [serverUrl="http://localhost:8000"] - OAuth server URL
126
+ * @property {string} [network="mainnet"] - Network type ("mainnet" or "testnet")
127
+ * @property {SecureStorage} [secureStorage] - Storage interface for tokens
128
+ * @property {Function} [fetch] - Custom fetch implementation (optional)
129
+ */
130
+
131
+ /**
132
+ * @typedef {Object} SecureStorage
133
+ * @property {function(string): string|null} getItem
134
+ * @property {function(string, string): void} setItem
135
+ * @property {function(string): void} removeItem
136
+ */
137
+
138
+ /**
139
+ * @typedef {Object} AuthenticationResult
140
+ * @property {string} access_token - JWT access token
141
+ * @property {string} refresh_token - Refresh token
142
+ * @property {number} expires_in - Token expiration in seconds
143
+ * @property {string} token_type - Token type (e.g., "bearer")
144
+ */
145
+
146
+ /**
147
+ * Bitcoin Cash OAuth Client
148
+ * Universal client library for browser and Node.js
149
+ */
150
+ class BitcoinCashOAuthClient {
151
+ /**
152
+ * Create a new OAuth client instance
153
+ * @param {OAuthClientOptions} options - Configuration options
154
+ */
155
+ constructor(options = {}) {
156
+ this.serverUrl = options.serverUrl || "http://localhost:8000";
157
+ this.network = options.network || "mainnet";
158
+ this.secureStorage = options.secureStorage || null;
159
+ this.fetchImpl = options.fetch || getFetch();
160
+ this.secp256k1 = null;
161
+ }
162
+
163
+ /**
164
+ * Initialize the client by instantiating secp256k1
165
+ * @returns {Promise<BitcoinCashOAuthClient>} The initialized client instance
166
+ */
167
+ async init() {
168
+ if (!this.secp256k1) {
169
+ this.secp256k1 = await libauth.instantiateSecp256k1();
170
+ }
171
+ return this;
172
+ }
173
+
174
+ /**
175
+ * Generate a new Bitcoin Cash keypair
176
+ * @returns {Promise<Keypair>} Keypair object with privateKey, publicKey, and address
177
+ */
178
+ async generateKeypair() {
179
+ await this.init();
180
+
181
+ // Generate random bytes for private key
182
+ const randomBytes = await getRandomBytes(32);
183
+
184
+ // libauth's generatePrivateKey expects a function that returns Uint8Array
185
+ // We create a closure that returns our pre-generated random bytes
186
+ const secureRandom = () => randomBytes;
187
+
188
+ // Generate private key (32 bytes)
189
+ const privateKeyBytes = libauth.generatePrivateKey(secureRandom);
190
+
191
+ // Derive compressed public key (33 bytes)
192
+ const publicKeyBytes = this.secp256k1.derivePublicKeyCompressed(privateKeyBytes);
193
+
194
+ // Convert to address
195
+ const address = await this.publicKeyToCashAddress(publicKeyBytes);
196
+
197
+ return {
198
+ privateKey: this.bytesToHex(privateKeyBytes),
199
+ publicKey: this.bytesToHex(publicKeyBytes),
200
+ address,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Convert public key to Bitcoin Cash CashAddr format
206
+ * @param {Uint8Array} publicKey - Compressed public key
207
+ * @returns {Promise<string>} Bitcoin Cash CashAddr address
208
+ */
209
+ async publicKeyToCashAddress(publicKey) {
210
+ // Hash public key: RIPEMD160(SHA256(publicKey))
211
+ const sha256Hash = await sha256(publicKey);
212
+ const ripemd160Hash = await ripemd160(sha256Hash);
213
+
214
+ // Determine network prefix
215
+ const prefix = this.network === "mainnet"
216
+ ? libauth.CashAddressNetworkPrefix.mainnet
217
+ : libauth.CashAddressNetworkPrefix.testnet;
218
+
219
+ // Encode as CashAddr (P2PKH type)
220
+ const address = libauth.encodeCashAddress(prefix, libauth.CashAddressType.P2PKH, ripemd160Hash);
221
+
222
+ if (typeof address !== 'string') {
223
+ throw new Error(`Failed to encode CashAddress: ${address}`);
224
+ }
225
+
226
+ return address;
227
+ }
228
+
229
+ /**
230
+ * Convert bytes to hex string
231
+ * @param {Uint8Array} bytes
232
+ * @returns {string}
233
+ */
234
+ bytesToHex(bytes) {
235
+ return Array.from(bytes)
236
+ .map(b => b.toString(16).padStart(2, "0"))
237
+ .join("");
238
+ }
239
+
240
+ /**
241
+ * Convert hex string to bytes
242
+ * @param {string} hex
243
+ * @returns {Uint8Array}
244
+ */
245
+ hexToBytes(hex) {
246
+ const bytes = new Uint8Array(hex.length / 2);
247
+ for (let i = 0; i < hex.length; i += 2) {
248
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
249
+ }
250
+ return bytes;
251
+ }
252
+
253
+ /**
254
+ * Register a new user with the server
255
+ * @param {string} address - Bitcoin Cash address
256
+ * @param {string} [userId] - Optional user-provided ID
257
+ * @returns {Promise<Object>} Registration result with assigned userId
258
+ */
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}`);
274
+ }
275
+
276
+ return await response.json();
277
+ }
278
+
279
+ /**
280
+ * Create authentication message (protocol|domain|userId|timestamp)
281
+ * @param {string} userId
282
+ * @param {number} [timestamp] - Unix timestamp (defaults to now)
283
+ * @param {string} [domain] - Domain/host (defaults to window.location.host or 'oauth')
284
+ * @returns {string}
285
+ */
286
+ createAuthMessage(userId, timestamp = null, domain = null) {
287
+ const ts = timestamp || Math.floor(Date.now() / 1000);
288
+ const host = domain || (typeof window !== 'undefined' && window?.location?.host) || 'oauth';
289
+ return `bitcoincash-oauth|${host}|${userId}|${ts}`;
290
+ }
291
+
292
+ /**
293
+ * Sign authentication message with private key
294
+ * @param {string} message - The message to sign (userId,timestamp)
295
+ * @param {string} privateKeyHex - Hex-encoded private key
296
+ * @returns {Promise<string>} DER-encoded signature in hex
297
+ */
298
+ async signAuthMessage(message, privateKeyHex) {
299
+ await this.init();
300
+
301
+ const privateKey = this.hexToBytes(privateKeyHex);
302
+
303
+ // Hash the message using SHA256
304
+ const messageBytes = new TextEncoder().encode(message);
305
+ const messageHash = await sha256(messageBytes);
306
+
307
+ // Sign using secp256k1
308
+ const signature = this.secp256k1.signMessageHashDER(privateKey, messageHash);
309
+
310
+ return this.bytesToHex(signature);
311
+ }
312
+
313
+ /**
314
+ * Authenticate with the server
315
+ * @param {string} userId
316
+ * @param {string} privateKeyHex
317
+ * @param {string} publicKeyHex
318
+ * @param {number} [timestamp] - Optional timestamp
319
+ * @param {string} [domain] - Optional domain for message binding
320
+ * @returns {Promise<AuthenticationResult>} Authentication result with access_token
321
+ */
322
+ async authenticate(userId, privateKeyHex, publicKeyHex, timestamp = null, domain = null) {
323
+ const ts = timestamp || Math.floor(Date.now() / 1000);
324
+ const message = this.createAuthMessage(userId, ts, domain);
325
+ const signature = await this.signAuthMessage(message, privateKeyHex);
326
+
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}`);
343
+ }
344
+
345
+ const result = await response.json();
346
+
347
+ // Store token if secure storage is available
348
+ if (this.secureStorage && result.access_token) {
349
+ this.secureStorage.setItem("oauth_token", result.access_token);
350
+ }
351
+
352
+ return result;
353
+ }
354
+
355
+ /**
356
+ * Get stored token
357
+ * @returns {string|null}
358
+ */
359
+ getToken() {
360
+ if (this.secureStorage) {
361
+ return this.secureStorage.getItem("oauth_token");
362
+ }
363
+ return null;
364
+ }
365
+
366
+ /**
367
+ * Make authenticated request
368
+ * @param {string} endpoint - API endpoint (relative to serverUrl)
369
+ * @param {Object} [options] - Fetch options
370
+ * @returns {Promise<Response>}
371
+ */
372
+ async authenticatedRequest(endpoint, options = {}) {
373
+ const token = this.getToken();
374
+
375
+ if (!token) {
376
+ throw new Error("No authentication token available");
377
+ }
378
+
379
+ const headers = {
380
+ "Authorization": `Bearer ${token}`,
381
+ ...options.headers,
382
+ };
383
+
384
+ return this.fetchImpl(`${this.serverUrl}${endpoint}`, {
385
+ ...options,
386
+ headers,
387
+ });
388
+ }
389
+
390
+ /**
391
+ * Refresh access token
392
+ * @param {string} refreshToken
393
+ * @returns {Promise<AuthenticationResult>}
394
+ */
395
+ 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}`);
409
+ }
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
+ }
419
+
420
+ /**
421
+ * Revoke token
422
+ * @param {string} token
423
+ * @returns {Promise<Object>}
424
+ */
425
+ 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}`);
439
+ }
440
+
441
+ if (this.secureStorage) {
442
+ this.secureStorage.removeItem("oauth_token");
443
+ }
444
+
445
+ return await response.json();
446
+ }
447
+ }
448
+
449
+ exports.BitcoinCashOAuthClient = BitcoinCashOAuthClient;
450
+ exports.default = BitcoinCashOAuthClient;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Bitcoin Cash OAuth Client
3
+ * TypeScript type definitions
4
+ */
5
+
6
+ export interface Keypair {
7
+ /** Hex-encoded private key */
8
+ privateKey: string;
9
+ /** Hex-encoded compressed public key */
10
+ publicKey: string;
11
+ /** Bitcoin Cash address */
12
+ address: string;
13
+ }
14
+
15
+ export interface SecureStorage {
16
+ getItem(key: string): string | null;
17
+ setItem(key: string, value: string): void;
18
+ removeItem(key: string): void;
19
+ }
20
+
21
+ export interface OAuthClientOptions {
22
+ /** OAuth server URL (default: "http://localhost:8000") */
23
+ serverUrl?: string;
24
+ /** Network type - "mainnet" or "testnet" (default: "mainnet") */
25
+ network?: 'mainnet' | 'testnet';
26
+ /** Storage interface for tokens */
27
+ secureStorage?: SecureStorage;
28
+ /** Custom fetch implementation (optional) */
29
+ fetch?: typeof fetch;
30
+ }
31
+
32
+ export interface AuthenticationResult {
33
+ /** JWT access token */
34
+ access_token: string;
35
+ /** Refresh token */
36
+ refresh_token: string;
37
+ /** Token expiration in seconds */
38
+ expires_in: number;
39
+ /** Token type (e.g., "bearer") */
40
+ token_type: string;
41
+ }
42
+
43
+ export interface RegistrationResult {
44
+ /** Assigned user ID */
45
+ user_id: string;
46
+ /** Registration status message */
47
+ message?: string;
48
+ }
49
+
50
+ export class BitcoinCashOAuthClient {
51
+ constructor(options?: OAuthClientOptions);
52
+
53
+ /** Initialize the client by instantiating secp256k1 */
54
+ init(): Promise<BitcoinCashOAuthClient>;
55
+
56
+ /** Generate a new Bitcoin Cash keypair */
57
+ generateKeypair(): Promise<Keypair>;
58
+
59
+ /** Convert public key to Bitcoin Cash CashAddr format */
60
+ publicKeyToCashAddress(publicKey: Uint8Array): Promise<string>;
61
+
62
+ /** Register a new user with the server */
63
+ register(address: string, userId?: string | null): Promise<RegistrationResult>;
64
+
65
+ /** Create authentication message (userId,timestamp) */
66
+ createAuthMessage(userId: string, timestamp?: number | null): string;
67
+
68
+ /** Sign authentication message with private key */
69
+ signAuthMessage(message: string, privateKeyHex: string): Promise<string>;
70
+
71
+ /** Authenticate with the server */
72
+ authenticate(
73
+ userId: string,
74
+ privateKeyHex: string,
75
+ publicKeyHex: string,
76
+ timestamp?: number | null
77
+ ): Promise<AuthenticationResult>;
78
+
79
+ /** Get stored token */
80
+ getToken(): string | null;
81
+
82
+ /** Make authenticated request */
83
+ authenticatedRequest(endpoint: string, options?: RequestInit): Promise<Response>;
84
+
85
+ /** Refresh access token */
86
+ refreshToken(refreshToken: string): Promise<AuthenticationResult>;
87
+
88
+ /** Revoke token */
89
+ revokeToken(token: string): Promise<{ message: string }>;
90
+
91
+ // Utility methods
92
+ bytesToHex(bytes: Uint8Array): string;
93
+ hexToBytes(hex: string): Uint8Array;
94
+ sha256(data: Uint8Array): Promise<Uint8Array>;
95
+ ripemd160(data: Uint8Array): Promise<Uint8Array>;
96
+ }
97
+
98
+ export default BitcoinCashOAuthClient;