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