@profullstack/coinpay 0.8.0 → 0.10.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.8.0",
4
- "description": "CoinPay SDK & CLI — Accept cryptocurrency payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet and swap support",
3
+ "version": "0.10.0",
4
+ "description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
@@ -48,6 +48,29 @@
48
48
  },
49
49
  "./x402-pay-server": {
50
50
  "import": "./src/x402-pay-server.js"
51
+ },
52
+ "./taskmarket": {
53
+ "import": "./src/taskmarket.js"
54
+ },
55
+ "./finances": {
56
+ "import": "./src/finances.js",
57
+ "types": "./src/finances.d.ts"
58
+ },
59
+ "./finances-tui": {
60
+ "import": "./src/finances-tui.js"
61
+ },
62
+ "./finances-reports": {
63
+ "types": "./src/finances-reports.d.ts",
64
+ "import": "./src/finances-reports.js",
65
+ "default": "./src/finances-reports.js"
66
+ },
67
+ "./finances-commands": {
68
+ "import": "./src/finances-commands.js",
69
+ "default": "./src/finances-commands.js"
70
+ },
71
+ "./finances-report": {
72
+ "import": "./src/finances-report.js",
73
+ "default": "./src/finances-report.js"
51
74
  }
52
75
  },
53
76
  "files": [
@@ -86,7 +109,12 @@
86
109
  "payment-gateway",
87
110
  "merchant",
88
111
  "bip39",
89
- "mnemonic"
112
+ "mnemonic",
113
+ "finances",
114
+ "tui",
115
+ "dashboard",
116
+ "plaid",
117
+ "simplefin"
90
118
  ],
91
119
  "author": "Profullstack, Inc.",
92
120
  "license": "MIT",
@@ -103,16 +131,16 @@
103
131
  "node": ">=20.0.0"
104
132
  },
105
133
  "devDependencies": {
106
- "vitest": "^4.1.0",
107
134
  "eslint": "^8.57.0",
108
- "prettier": "^3.4.1"
135
+ "prettier": "^3.4.1",
136
+ "vitest": "^4.1.11"
109
137
  },
110
- "peerDependencies": {},
111
138
  "dependencies": {
112
- "@scure/bip39": "^1.4.0",
113
- "@scure/bip32": "^1.5.0",
139
+ "@inquirer/prompts": "^8.3.0",
114
140
  "@noble/curves": "^1.6.0",
115
141
  "@noble/hashes": "^1.5.0",
116
- "@inquirer/prompts": "^8.3.0"
142
+ "@profullstack/hqtui": "^0.1.11",
143
+ "@scure/bip32": "^1.5.0",
144
+ "@scure/bip39": "^1.4.0"
117
145
  }
118
146
  }
@@ -309,13 +309,21 @@ export async function createCardEscrow(client, businessId, amountUSD, descriptio
309
309
  // Convert USD to cents
310
310
  const amountCents = Math.round(amountUSD * 100);
311
311
 
312
- return client.request('POST', '/api/stripe/payments/create', {
313
- businessId,
314
- amount: amountCents,
315
- currency: 'usd',
316
- description,
317
- metadata,
318
- escrowMode: true,
312
+ // `CoinPayClient.request(endpoint, options)` takes TWO arguments. Every call
313
+ // in this file passed three, in the shape `request(method, path, body)` — so
314
+ // `endpoint` received 'POST', the URL became `baseUrl + 'POST'`, and `options`
315
+ // received the path as a string. These calls failed 100% of the time, which
316
+ // made releasing or refunding a card escrow unreachable through the SDK.
317
+ return client.request('/api/stripe/payments/create', {
318
+ method: 'POST',
319
+ body: JSON.stringify({
320
+ businessId,
321
+ amount: amountCents,
322
+ currency: 'usd',
323
+ description,
324
+ metadata,
325
+ escrowMode: true,
326
+ }),
319
327
  });
320
328
  }
321
329
 
@@ -346,7 +354,7 @@ export async function listCardEscrows(client, options = {}) {
346
354
  const queryString = queryParams.toString();
347
355
  const url = `/api/stripe/escrows${queryString ? `?${queryString}` : ''}`;
348
356
 
349
- return client.request('GET', url);
357
+ return client.request(url, { method: 'GET' });
350
358
  }
351
359
 
352
360
  /**
@@ -360,8 +368,9 @@ export async function listCardEscrows(client, options = {}) {
360
368
  * const result = await releaseCardEscrow(client, 'escrow-123');
361
369
  */
362
370
  export async function releaseCardEscrow(client, escrowId) {
363
- return client.request('POST', '/api/stripe/escrow/release', {
364
- escrowId,
371
+ return client.request('/api/stripe/escrow/release', {
372
+ method: 'POST',
373
+ body: JSON.stringify({ escrowId }),
365
374
  });
366
375
  }
367
376
 
@@ -386,9 +395,9 @@ export async function releaseCardEscrow(client, escrowId) {
386
395
  * });
387
396
  */
388
397
  export async function refundCardEscrow(client, escrowId, options = {}) {
389
- return client.request('POST', '/api/stripe/escrow/refund', {
390
- escrowId,
391
- ...options,
398
+ return client.request('/api/stripe/escrow/refund', {
399
+ method: 'POST',
400
+ body: JSON.stringify({ escrowId, ...options }),
392
401
  });
393
402
  }
394
403
 
@@ -404,7 +413,7 @@ export async function refundCardEscrow(client, escrowId, options = {}) {
404
413
  * console.log(status.escrow_status); // 'pending', 'released', 'refunded'
405
414
  */
406
415
  export async function getCardEscrowStatus(client, escrowId) {
407
- return client.request('GET', `/api/stripe/transactions/${escrowId}`);
416
+ return client.request(`/api/stripe/transactions/${escrowId}`, { method: 'GET' });
408
417
  }
409
418
 
410
419
  /**
package/src/client.d.ts CHANGED
@@ -313,6 +313,16 @@ export class CoinPayClient {
313
313
  * @param eventType - Event type to simulate (default: `'payment.completed'`)
314
314
  */
315
315
  testWebhook(businessId: string, eventType?: string): Promise<Record<string, unknown>>;
316
+ /** Fetch a binary body (PDF, CSV) with its describing headers. */
317
+ requestBinary(endpoint: string, options?: RequestInit): Promise<{
318
+ bytes: Uint8Array;
319
+ contentType: string;
320
+ filename: string | null;
321
+ sha256: string | null;
322
+ headers: Record<string, string>;
323
+ }>;
324
+ /** POST a multipart form (file upload). */
325
+ requestForm(endpoint: string, form: FormData): Promise<any>;
316
326
  }
317
327
 
318
328
  export default CoinPayClient;
package/src/client.js CHANGED
@@ -77,6 +77,97 @@ export class CoinPayClient {
77
77
  }
78
78
  }
79
79
 
80
+ /**
81
+ * Fetch a binary body (a PDF, a CSV) rather than JSON.
82
+ *
83
+ * `request()` always parses JSON, which a file download is not. This
84
+ * returns the bytes plus the headers that describe them; an error body
85
+ * is still parsed as JSON so its `code` survives.
86
+ * @param {string} endpoint - API endpoint
87
+ * @param {Object} [options] - Fetch options
88
+ * @returns {Promise<{bytes: Uint8Array, contentType: string, filename: string|null, sha256: string|null, headers: Record<string,string>}>}
89
+ */
90
+ async requestBinary(endpoint, options = {}) {
91
+ if (!this.#apiKey) {
92
+ throw new Error('API key is required for authenticated requests.');
93
+ }
94
+ const url = `${this.#baseUrl}${endpoint}`;
95
+ const controller = new AbortController();
96
+ const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
97
+ try {
98
+ const response = await fetch(url, {
99
+ ...options,
100
+ signal: controller.signal,
101
+ headers: { 'Authorization': `Bearer ${this.#apiKey}`, ...options.headers },
102
+ });
103
+ if (!response.ok) {
104
+ let data = {};
105
+ try { data = await response.json(); } catch { /* not JSON */ }
106
+ const structured = data && data.error && typeof data.error === 'object' ? data.error : null;
107
+ const error = new Error((structured && structured.message) || data.error || `HTTP ${response.status}`);
108
+ error.status = response.status;
109
+ error.response = data;
110
+ if (structured && structured.code) error.code = structured.code;
111
+ throw error;
112
+ }
113
+ const headers = {};
114
+ response.headers.forEach((value, key) => { headers[key] = value; });
115
+ const disposition = headers['content-disposition'] || '';
116
+ const match = /filename="?([^";]+)"?/i.exec(disposition);
117
+ return {
118
+ bytes: new Uint8Array(await response.arrayBuffer()),
119
+ contentType: headers['content-type'] || 'application/octet-stream',
120
+ filename: match ? match[1] : null,
121
+ sha256: headers['x-content-sha256'] || null,
122
+ headers,
123
+ };
124
+ } catch (error) {
125
+ if (error.name === 'AbortError') throw new Error(`Request timeout after ${this.#timeout}ms`);
126
+ throw error;
127
+ } finally {
128
+ clearTimeout(timeoutId);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * POST a multipart form (a file upload). The browser/undici sets the
134
+ * boundary, so no Content-Type is forced here.
135
+ * @param {string} endpoint - API endpoint
136
+ * @param {FormData} form - The form to send
137
+ * @returns {Promise<Object>} API response
138
+ */
139
+ async requestForm(endpoint, form) {
140
+ if (!this.#apiKey) {
141
+ throw new Error('API key is required for authenticated requests.');
142
+ }
143
+ const url = `${this.#baseUrl}${endpoint}`;
144
+ const controller = new AbortController();
145
+ const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
146
+ try {
147
+ const response = await fetch(url, {
148
+ method: 'POST',
149
+ body: form,
150
+ signal: controller.signal,
151
+ headers: { 'Authorization': `Bearer ${this.#apiKey}` },
152
+ });
153
+ const data = await response.json();
154
+ if (!response.ok) {
155
+ const structured = data && data.error && typeof data.error === 'object' ? data.error : null;
156
+ const error = new Error((structured && structured.message) || data.error || `HTTP ${response.status}`);
157
+ error.status = response.status;
158
+ error.response = data;
159
+ if (structured && structured.code) error.code = structured.code;
160
+ throw error;
161
+ }
162
+ return data;
163
+ } catch (error) {
164
+ if (error.name === 'AbortError') throw new Error(`Request timeout after ${this.#timeout}ms`);
165
+ throw error;
166
+ } finally {
167
+ clearTimeout(timeoutId);
168
+ }
169
+ }
170
+
80
171
  /**
81
172
  * Make an unauthenticated API request (for registration/login)
82
173
  * @param {string} endpoint - API endpoint
@@ -454,23 +545,61 @@ export class CoinPayClient {
454
545
 
455
546
  /**
456
547
  * Create a new business
548
+ *
549
+ * Field names are snake_case because that is what the API reads. This used to
550
+ * send `webhookUrl`, which POST /api/businesses does not look at -- it reads
551
+ * `input.webhook_url` -- so every business created through the SDK came back
552
+ * with a null webhook_url and no webhook secret, silently. The value was
553
+ * accepted, echoed nowhere, and dropped.
554
+ *
555
+ * `category` is required by the API and was not sent at all, so
556
+ * `coinpay business create` failed outright with "Select a valid business
557
+ * category" and there was no flag to supply one. Valid slugs live in
558
+ * src/lib/business/taxonomy.ts.
559
+ *
457
560
  * @param {Object} params - Business parameters
458
561
  * @param {string} params.name - Business name
562
+ * @param {string} params.category - Taxonomy slug; required by the API
563
+ * @param {string} [params.description]
459
564
  * @param {string} [params.webhookUrl] - Webhook URL
565
+ * @param {string} [params.webhookSecret] - Supply one, or the API generates it
460
566
  * @param {Object} [params.walletAddresses] - Wallet addresses by chain
461
567
  * @returns {Promise<Object>} Created business
462
568
  */
463
- async createBusiness({ name, webhookUrl, walletAddresses }) {
569
+ async createBusiness({
570
+ name,
571
+ category,
572
+ description,
573
+ webhookUrl,
574
+ webhookSecret,
575
+ walletAddresses,
576
+ }) {
464
577
  return this.request('/businesses', {
465
578
  method: 'POST',
466
579
  body: JSON.stringify({
467
580
  name,
468
- webhookUrl,
581
+ category,
582
+ description,
583
+ webhook_url: webhookUrl,
584
+ webhook_secret: webhookSecret,
469
585
  walletAddresses,
470
586
  }),
471
587
  });
472
588
  }
473
589
 
590
+ /**
591
+ * The taxonomy a business may be created with.
592
+ *
593
+ * Fetched rather than bundled: a copy inside the SDK goes stale the moment a
594
+ * category is added, and a stale copy is how "Select a valid business category"
595
+ * happens a second time.
596
+ *
597
+ * @returns {Promise<Object>} { success, categories: [{ slug, label, group, baseRisk }] }
598
+ */
599
+ async listBusinessCategories() {
600
+ return this.request('/businesses/categories', { method: 'GET' });
601
+ }
602
+
474
603
  /**
475
604
  * Update business
476
605
  * @param {string} businessId - Business ID