@profullstack/coinpay 0.9.0 → 0.11.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/CHANGELOG.md +57 -0
- package/README.md +8 -0
- package/bin/coinpay.js +158 -13
- package/package.json +15 -2
- package/src/client.d.ts +10 -0
- package/src/client.js +91 -0
- package/src/finances-commands.js +613 -0
- package/src/finances-report.js +458 -0
- package/src/finances-reports.d.ts +359 -0
- package/src/finances-reports.js +356 -0
- package/src/finances-tui.js +14 -0
- package/src/finances.d.ts +32 -0
- package/src/finances.js +68 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +3 -0
- package/src/pdf.js +151 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,56 @@ All notable changes to `@profullstack/coinpay` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.11.0] - 2026-09-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `coinpay finances books queue|confirm|confirm-all|categorize|rules|summary|export|send`:
|
|
12
|
+
every transaction auto-categorised (rules, heuristics, and Claude when the
|
|
13
|
+
server has a key) with a review queue, an "always" flag that turns a
|
|
14
|
+
correction into a rule, a tax summary by year/quarter/month in exact
|
|
15
|
+
arithmetic, and the CPA pack as CSV/PDF/HTML/JSON.
|
|
16
|
+
- `coinpay finances payloads list|download`: every raw provider response,
|
|
17
|
+
exactly as received.
|
|
18
|
+
- `coinpay finances reports send <id> --to …` and `books send --to …`: email
|
|
19
|
+
a report or the pack with attachments plus an expiring no-login link.
|
|
20
|
+
- `coinpay finances digest set|show|off|send-now`: a weekly digest on chosen
|
|
21
|
+
weekdays at a chosen local hour.
|
|
22
|
+
- `coinpay finances consent <id> on|off`: opt a connection into the
|
|
23
|
+
30-minute background sync.
|
|
24
|
+
- SDK: `finances-reports` gained the books, payloads, email and digest
|
|
25
|
+
functions with declarations.
|
|
26
|
+
|
|
27
|
+
## [0.10.0] - 2026-09-12
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `coinpay finances report`: monthly, quarterly and custom-period activity
|
|
31
|
+
reports generated server-side from an immutable snapshot, downloadable as
|
|
32
|
+
PDF, HTML, CSV or JSON (`--period 2026-08`, `--period 2026-Q2`, or
|
|
33
|
+
`--from/--to` with an exclusive end; `--account`, `--scope`, `--timezone`,
|
|
34
|
+
`--format`, `--output`, `--strict`, `--wait`, `--json`). Every report
|
|
35
|
+
states that it was generated by CoinPay from imported account data and is
|
|
36
|
+
not an institution-issued statement, and carries `local_export_complete`,
|
|
37
|
+
`provider_coverage` and `reconciliation_status`.
|
|
38
|
+
- `coinpay finances backfill`, `jobs`, `coverage`: durable, resumable
|
|
39
|
+
calendar-period fetches in provider-safe chunks, with per-account coverage
|
|
40
|
+
derived from what was actually requested.
|
|
41
|
+
- `coinpay finances statements import|list|get|download|reconcile|delete`:
|
|
42
|
+
keep original bank statements (PDF, user supplied, never parsed) beside an
|
|
43
|
+
account and period, and check their balances against a report revision
|
|
44
|
+
with exact decimal arithmetic.
|
|
45
|
+
- `coinpay finances connect` (setup token prompted without echo or via
|
|
46
|
+
`--setup-token-stdin`), `disconnect`, `consent <id> on|off`.
|
|
47
|
+
- SDK: `finances-reports` module (`createFinanceReport`, `waitForFinanceReport`,
|
|
48
|
+
`downloadFinanceReport`, `createBackfillJob`, `getFinanceCoverage`,
|
|
49
|
+
`importFinanceStatement`, `reconcileFinanceStatement`, ...), and
|
|
50
|
+
`CoinPayClient.requestBinary()` / `requestForm()` for downloads and uploads.
|
|
51
|
+
- Exit codes for the new subcommands: 0 done or job accepted, 2 invalid
|
|
52
|
+
input, 3 strict/completeness rejection, 4 authentication, 5 provider or
|
|
53
|
+
storage failure. In `--json` mode stdout carries only the result.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- `listAllFinanceTransactions` now forwards `endDate`.
|
|
57
|
+
|
|
8
58
|
## [0.9.0] - 2026-09-05
|
|
9
59
|
|
|
10
60
|
### Added
|
|
@@ -31,6 +81,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
31
81
|
Built from about six months of history rather than the dashboard window,
|
|
32
82
|
because a monthly charge is invisible in thirty days of rows, and averaged
|
|
33
83
|
over the history that exists rather than the history requested.
|
|
84
|
+
- `coinpay finances scope <account-id> <business|personal|clear>`, and a `Side`
|
|
85
|
+
column on the accounts table. Which set of books an account belongs to was a
|
|
86
|
+
guess from its name, which reads "Business Checking" correctly and is blind to
|
|
87
|
+
a personal card carrying company spend. The correction is now stored in
|
|
88
|
+
`finance_accounts.scope_override`, wins over the guess, and survives a re-sync
|
|
89
|
+
— the same contract as `kind_override`. An overridden account is marked with a
|
|
90
|
+
trailing `*`, so a wrong split is traceable to a guess rather than a decision.
|
|
34
91
|
- Plain-text `coinpay finances summary|accounts|ledger|connections|sync`
|
|
35
92
|
(`--json` on all of them) for scripts and for Node older than 22.6.
|
|
36
93
|
- SDK `finances` module, also importable as `@profullstack/coinpay/finances`:
|
package/README.md
CHANGED
|
@@ -697,6 +697,7 @@ coinpay finances # live dashboard (needs `coinpay login`)
|
|
|
697
697
|
coinpay finances --days 90 # window for earnings / cashflow: 7, 30, 90, 365
|
|
698
698
|
coinpay finances summary # plain text, --json for machines
|
|
699
699
|
coinpay finances position # debt vs income, credits vs debits (also: debt)
|
|
700
|
+
coinpay finances scope <id> business # put an account on the company's books
|
|
700
701
|
coinpay finances accounts # linked bank & credit-card accounts (SimpleFIN / Plaid)
|
|
701
702
|
coinpay finances ledger --search anthropic --limit 20
|
|
702
703
|
coinpay finances connections # institutions and their last sync
|
|
@@ -718,6 +719,13 @@ instalment, months to clear each balance at its current payment rate, debt-to-in
|
|
|
718
719
|
debt-service ratio, months of cover, card utilisation, the recurring bills it found with
|
|
719
720
|
their next due date, and a business-versus-personal split of all of it.
|
|
720
721
|
|
|
722
|
+
The business/personal split starts as a guess from the account name, which gets
|
|
723
|
+
"Business Checking (4672)" right and cannot see a personal card carrying company
|
|
724
|
+
spend. Correct it with `coinpay finances scope <account-id> business|personal`,
|
|
725
|
+
or `clear` to hand it back to the guess. The correction is stored separately from
|
|
726
|
+
the derived value, so a re-sync cannot clobber it, and corrected accounts carry a
|
|
727
|
+
trailing `*` in the `Side` column.
|
|
728
|
+
|
|
721
729
|
Two things about those numbers. Transfers and card payments are excluded from both
|
|
722
730
|
income and spending — a feed holding both a checking account and the card it pays
|
|
723
731
|
contains every card payment twice, so counting raw credits as income inflates both
|
package/bin/coinpay.js
CHANGED
|
@@ -161,6 +161,18 @@ function createUnauthenticatedClient() {
|
|
|
161
161
|
|
|
162
162
|
const BOOLEAN_FLAGS = new Set([
|
|
163
163
|
'active',
|
|
164
|
+
// finances reports / statements
|
|
165
|
+
'wait',
|
|
166
|
+
'strict',
|
|
167
|
+
'setup-token-stdin',
|
|
168
|
+
'overwrite',
|
|
169
|
+
'no-pending',
|
|
170
|
+
'liability-positive',
|
|
171
|
+
'acknowledge',
|
|
172
|
+
'always',
|
|
173
|
+
'no-model',
|
|
174
|
+
'only-uncategorized',
|
|
175
|
+
'no-attach',
|
|
164
176
|
'plain',
|
|
165
177
|
'no-stream',
|
|
166
178
|
'hidden',
|
|
@@ -450,10 +462,37 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
450
462
|
summary Plain-text headline numbers (--json for machines)
|
|
451
463
|
position Debt vs income, credits vs debits, recurring bills
|
|
452
464
|
(also: coinpay finances debt)
|
|
465
|
+
scope <id> <side> Put an account on the business or personal books
|
|
466
|
+
(side: business | personal | clear)
|
|
453
467
|
accounts Linked bank and card accounts with balances
|
|
454
468
|
ledger Bank/card transactions (--limit, --search, --category, --account)
|
|
455
469
|
connections Linked institutions and their last sync
|
|
456
470
|
sync Pull fresh balances from the bank bridge (--days)
|
|
471
|
+
connect Link SimpleFIN: token prompted without echo, or --setup-token-stdin
|
|
472
|
+
disconnect <id> Stop syncs and drop the credential; history stays (--yes)
|
|
473
|
+
consent <id> on|off Opt a connection into a once-daily background sync
|
|
474
|
+
backfill Fetch a calendar period in safe chunks (--period 2026-Q2 |
|
|
475
|
+
--from/--to, --connection, --timezone, --wait, --json)
|
|
476
|
+
jobs [get|cancel <id>] Persisted sync/report jobs; reading one costs no request
|
|
477
|
+
coverage What part of a period was ever fetched (--period, --account)
|
|
478
|
+
report Monthly/quarterly/custom activity report (--period 2026-08 |
|
|
479
|
+
--from/--to; --account, --scope, --timezone, --format pdf|csv|html|json,
|
|
480
|
+
--output <file>, --strict, --wait, --json)
|
|
481
|
+
reports [get|download|delete <id>] Report history and downloads
|
|
482
|
+
statements import <pdf> Keep an original bank statement (--account, --period|--from/--to)
|
|
483
|
+
statements list|get|download|reconcile|delete
|
|
484
|
+
Statement library; reconcile takes --report --opening --closing --currency
|
|
485
|
+
books [queue] Transactions awaiting category review (--status, --scope, --search)
|
|
486
|
+
books confirm <id> Confirm a row (--category, --tax, --scope, --note, --always makes a rule)
|
|
487
|
+
books confirm-all Accept every pending suggestion (--yes)
|
|
488
|
+
books categorize Rules, heuristics, then the model over unreviewed rows (--wait)
|
|
489
|
+
books rules [add|delete] Merchant category rules
|
|
490
|
+
books summary Totals by tax category (--period 2026|2026-Q3|2026-08, --scope)
|
|
491
|
+
books export CPA pack (--period, --scope, --format csv|pdf|html|json, --output)
|
|
492
|
+
payloads [list|download <id>] Raw provider responses, exactly as received
|
|
493
|
+
reports send <id> Email a report (--to a@x,b@y, --format pdf,csv, --message, --no-attach)
|
|
494
|
+
books send Email the CPA pack (--to, --period, --scope, --format)
|
|
495
|
+
digest [show|set|off|send-now] Weekly digest email (--days mon,fri --hour 8 --to …)
|
|
457
496
|
|
|
458
497
|
${colors.bright}escrow${colors.reset}
|
|
459
498
|
create Create a new escrow
|
|
@@ -3486,7 +3525,7 @@ const MENU_COMMANDS = [
|
|
|
3486
3525
|
const SUBCOMMANDS = {
|
|
3487
3526
|
config: [['set-key', 'Set your API key'], ['set-url', 'Set custom API URL'], ['show', 'Show current configuration']],
|
|
3488
3527
|
auth: [['register', 'Register new merchant account'], ['login', 'Login to merchant account'], ['me', 'Show current merchant info']],
|
|
3489
|
-
finances: [['tui', 'Live dashboard (default)'], ['summary', 'Headline numbers as text'], ['position', 'Debt vs income, credits vs debits'], ['accounts', 'Bank & card accounts'], ['ledger', 'Bank/card transactions'], ['connections', 'Linked institutions'], ['sync', 'Pull fresh bank data']],
|
|
3528
|
+
finances: [['tui', 'Live dashboard (default)'], ['summary', 'Headline numbers as text'], ['position', 'Debt vs income, credits vs debits'], ['scope', 'Set an account business or personal'], ['accounts', 'Bank & card accounts'], ['ledger', 'Bank/card transactions'], ['connections', 'Linked institutions'], ['sync', 'Pull fresh bank data'], ['report', 'Monthly/quarterly activity report'], ['backfill', 'Fetch a calendar period'], ['statements', 'Original bank statements'], ['jobs', 'Sync and report jobs']],
|
|
3490
3529
|
payment: [['create', 'Create a new payment'], ['get', 'Get payment details <id>'], ['list', 'List payments'], ['qr', 'Get payment QR code <id>']],
|
|
3491
3530
|
invoice: [['create', 'Create a draft invoice'], ['list', 'List invoices'], ['get', 'Get invoice details <id>'], ['update', 'Update a draft invoice <id>'], ['publish', 'Publish an invoice <id>'], ['send', 'Send an invoice <id>'], ['delete', 'Delete a draft invoice <id>']],
|
|
3492
3531
|
tokens: [['list', 'List checkout tokens']],
|
|
@@ -4023,6 +4062,58 @@ async function handleFinances(subcommand, args, flags) {
|
|
|
4023
4062
|
case 'dashboard':
|
|
4024
4063
|
case 'watch': {
|
|
4025
4064
|
const { client, token, baseUrl } = financesClient();
|
|
4065
|
+
|
|
4066
|
+
// A document for somebody who bills by the hour. Every question it
|
|
4067
|
+
// answers up front is a question nobody pays to have asked, so it
|
|
4068
|
+
// defaults to the whole transaction list and a year of history.
|
|
4069
|
+
if (flags.pdf || flags.report) {
|
|
4070
|
+
const reportDays = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : 365;
|
|
4071
|
+
const limit = Number.parseInt(String(flags.limit ?? ''), 10);
|
|
4072
|
+
const snapshot = await fin.collectFinanceSnapshot(client, {
|
|
4073
|
+
days: reportDays,
|
|
4074
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 5000,
|
|
4075
|
+
businessId: flags['business-id'],
|
|
4076
|
+
// Page the whole window. Asking for 5000 returns 500, and a report
|
|
4077
|
+
// built on one page understates the period without saying so.
|
|
4078
|
+
allTransactions: true,
|
|
4079
|
+
});
|
|
4080
|
+
const { buildFinanceReportHtml } = await import('../src/finances-report.js');
|
|
4081
|
+
const html = buildFinanceReportHtml(snapshot, {
|
|
4082
|
+
preparedFor: typeof flags.for === 'string' ? flags.for : '',
|
|
4083
|
+
preparedBy: typeof flags.by === 'string' ? flags.by : '',
|
|
4084
|
+
entity: typeof flags.entity === 'string' ? flags.entity : '',
|
|
4085
|
+
includeTransactions: !flags['no-transactions'],
|
|
4086
|
+
transactionLimit: 0,
|
|
4087
|
+
title: typeof flags.title === 'string' ? flags.title : 'Financial summary',
|
|
4088
|
+
});
|
|
4089
|
+
|
|
4090
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
4091
|
+
const base = typeof flags.pdf === 'string' && flags.pdf
|
|
4092
|
+
? String(flags.pdf).replace(/\.pdf$/i, '')
|
|
4093
|
+
: `finances-${stamp}`;
|
|
4094
|
+
const pdfPath = `${base}.pdf`;
|
|
4095
|
+
const htmlOut = `${base}.html`;
|
|
4096
|
+
|
|
4097
|
+
const { writeFileSync } = await import('node:fs');
|
|
4098
|
+
writeFileSync(htmlOut, html, 'utf8');
|
|
4099
|
+
|
|
4100
|
+
const { htmlToPdf, installHint } = await import('../src/pdf.js');
|
|
4101
|
+
const result = await htmlToPdf(html, pdfPath);
|
|
4102
|
+
if (result.ok) {
|
|
4103
|
+
console.log(`Wrote ${pdfPath}`);
|
|
4104
|
+
console.log(` ${htmlOut}`);
|
|
4105
|
+
} else {
|
|
4106
|
+
// The HTML is the report. Losing the PDF is a downgrade, not a failure.
|
|
4107
|
+
console.log(`Wrote ${htmlOut}`);
|
|
4108
|
+
console.log('');
|
|
4109
|
+
console.log('Could not render a PDF on this machine.');
|
|
4110
|
+
console.log(installHint(result.tried));
|
|
4111
|
+
console.log('');
|
|
4112
|
+
console.log('Or open the HTML in a browser and print it to PDF.');
|
|
4113
|
+
}
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4026
4117
|
const wantsText = flags.plain || flags.json || !process.stdout.isTTY || !process.stdin.isTTY;
|
|
4027
4118
|
if (wantsText) {
|
|
4028
4119
|
const snapshot = await fin.collectFinanceSnapshot(client, { days, businessId: flags['business-id'] });
|
|
@@ -4073,6 +4164,30 @@ async function handleFinances(subcommand, args, flags) {
|
|
|
4073
4164
|
return;
|
|
4074
4165
|
}
|
|
4075
4166
|
|
|
4167
|
+
case 'scope': {
|
|
4168
|
+
// Which set of books an account belongs to. The name-derived guess is
|
|
4169
|
+
// right about "Business Checking" and blind to a personal card carrying
|
|
4170
|
+
// company spend, so the correction is stored and always wins.
|
|
4171
|
+
const { client } = financesClient();
|
|
4172
|
+
const accountId = args[0];
|
|
4173
|
+
const value = (args[1] || '').toLowerCase();
|
|
4174
|
+
if (!accountId) {
|
|
4175
|
+
print.error('Usage: coinpay finances scope <account-id> <business|personal|clear> (ids: coinpay finances accounts)');
|
|
4176
|
+
process.exit(1);
|
|
4177
|
+
}
|
|
4178
|
+
const scope = value === 'clear' || value === 'auto' || value === '' ? null : value;
|
|
4179
|
+
if (scope !== null && scope !== 'business' && scope !== 'personal') {
|
|
4180
|
+
print.error(`Unknown scope '${value}'. Use business, personal, or clear.`);
|
|
4181
|
+
process.exit(1);
|
|
4182
|
+
}
|
|
4183
|
+
const account = await fin.updateFinanceAccount(client, accountId, { scope });
|
|
4184
|
+
if (flags.json) { print.json({ account }); return; }
|
|
4185
|
+
print.success(
|
|
4186
|
+
`${account.name} → ${account.effective_scope}${scope === null ? ' (derived from the name again)' : ''}`,
|
|
4187
|
+
);
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4076
4191
|
case 'accounts': {
|
|
4077
4192
|
const { client } = financesClient();
|
|
4078
4193
|
const accounts = await fin.listFinanceAccounts(client, { includeHidden: Boolean(flags.hidden) });
|
|
@@ -4084,6 +4199,7 @@ async function handleFinances(subcommand, args, flags) {
|
|
|
4084
4199
|
{ title: 'Institution', render: (a) => a.org_name || '—' },
|
|
4085
4200
|
{ title: 'Account', render: (a) => a.name },
|
|
4086
4201
|
{ title: 'Kind', render: (a) => a.effective_kind || a.kind },
|
|
4202
|
+
{ title: 'Side', render: (a) => `${a.effective_scope || '—'}${a.scope_override ? '*' : ''}` },
|
|
4087
4203
|
{ title: 'Balance', align: 'right', render: (a) => money(a.display_balance ?? a.balance ?? 0, a.currency || 'USD') },
|
|
4088
4204
|
{ title: 'Available', align: 'right', render: (a) => (a.available_balance == null ? '—' : money(a.available_balance, a.currency || 'USD')) },
|
|
4089
4205
|
{ title: 'As of', render: (a) => shortDate(a.balance_date) },
|
|
@@ -4162,9 +4278,32 @@ async function handleFinances(subcommand, args, flags) {
|
|
|
4162
4278
|
return;
|
|
4163
4279
|
}
|
|
4164
4280
|
|
|
4281
|
+
case 'connect':
|
|
4282
|
+
case 'disconnect':
|
|
4283
|
+
case 'consent':
|
|
4284
|
+
case 'backfill':
|
|
4285
|
+
case 'jobs':
|
|
4286
|
+
case 'coverage':
|
|
4287
|
+
case 'report':
|
|
4288
|
+
case 'reports':
|
|
4289
|
+
case 'statements':
|
|
4290
|
+
case 'books':
|
|
4291
|
+
case 'payloads':
|
|
4292
|
+
case 'digest': {
|
|
4293
|
+
const { client } = financesClient();
|
|
4294
|
+
const { runFinancesCommand } = await import('../src/finances-commands.js');
|
|
4295
|
+
const code = await runFinancesCommand(subcommand, args, flags, {
|
|
4296
|
+
client,
|
|
4297
|
+
out: (line) => console.log(line),
|
|
4298
|
+
err: (line) => console.error(line),
|
|
4299
|
+
});
|
|
4300
|
+
if (code !== 0) process.exit(code);
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4165
4304
|
default:
|
|
4166
4305
|
print.error(`Unknown finances command: ${subcommand}`);
|
|
4167
|
-
console.log('Usage: coinpay finances [tui|summary|accounts|ledger|connections|sync]');
|
|
4306
|
+
console.log('Usage: coinpay finances [tui|summary|accounts|ledger|connections|sync|connect|disconnect|backfill|jobs|coverage|report|reports|statements|books|payloads|digest]');
|
|
4168
4307
|
process.exit(1);
|
|
4169
4308
|
}
|
|
4170
4309
|
}
|
|
@@ -4508,12 +4647,16 @@ async function handleOAuth(subcommand, args, flags) {
|
|
|
4508
4647
|
description,
|
|
4509
4648
|
});
|
|
4510
4649
|
|
|
4650
|
+
// The API wraps the record: { success, client: { ..., client_secret }, warning }.
|
|
4651
|
+
// Accept a bare record too so either shape prints.
|
|
4652
|
+
const created = result.client ?? result;
|
|
4653
|
+
|
|
4511
4654
|
print.success('OAuth client created');
|
|
4512
|
-
if (
|
|
4513
|
-
console.log(`\n ${colors.bright}Client ID:${colors.reset} ${
|
|
4655
|
+
if (created.client_id) {
|
|
4656
|
+
console.log(`\n ${colors.bright}Client ID:${colors.reset} ${created.client_id}`);
|
|
4514
4657
|
}
|
|
4515
|
-
if (
|
|
4516
|
-
console.log(` ${colors.bright}Client Secret:${colors.reset} ${colors.yellow}${
|
|
4658
|
+
if (created.client_secret) {
|
|
4659
|
+
console.log(` ${colors.bright}Client Secret:${colors.reset} ${colors.yellow}${created.client_secret}${colors.reset}`);
|
|
4517
4660
|
console.log();
|
|
4518
4661
|
print.warn('Save the client secret — it is only shown once!');
|
|
4519
4662
|
}
|
|
@@ -4531,13 +4674,15 @@ async function handleOAuth(subcommand, args, flags) {
|
|
|
4531
4674
|
}
|
|
4532
4675
|
|
|
4533
4676
|
const result = await client.getOAuthClient(id);
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
if (
|
|
4539
|
-
if (
|
|
4540
|
-
if (
|
|
4677
|
+
// GET /oauth/clients/:id also wraps the record as { success, client }.
|
|
4678
|
+
const found = result.client ?? result;
|
|
4679
|
+
|
|
4680
|
+
print.success(`OAuth Client: ${found.name || id}`);
|
|
4681
|
+
if (found.client_id) print.info(` Client ID: ${found.client_id}`);
|
|
4682
|
+
if (found.description) print.info(` Description: ${found.description}`);
|
|
4683
|
+
if (found.redirect_uris) print.info(` Redirect URIs: ${found.redirect_uris.join(', ')}`);
|
|
4684
|
+
if (found.scopes) print.info(` Scopes: ${Array.isArray(found.scopes) ? found.scopes.join(', ') : found.scopes}`);
|
|
4685
|
+
if (found.created_at) print.info(` Created: ${found.created_at}`);
|
|
4541
4686
|
|
|
4542
4687
|
if (flags.json) print.json(result);
|
|
4543
4688
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/coinpay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
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",
|
|
@@ -58,6 +58,19 @@
|
|
|
58
58
|
},
|
|
59
59
|
"./finances-tui": {
|
|
60
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"
|
|
61
74
|
}
|
|
62
75
|
},
|
|
63
76
|
"files": [
|
|
@@ -120,7 +133,7 @@
|
|
|
120
133
|
"devDependencies": {
|
|
121
134
|
"eslint": "^8.57.0",
|
|
122
135
|
"prettier": "^3.4.1",
|
|
123
|
-
"vitest": "^4.1.
|
|
136
|
+
"vitest": "^4.1.11"
|
|
124
137
|
},
|
|
125
138
|
"dependencies": {
|
|
126
139
|
"@inquirer/prompts": "^8.3.0",
|
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
|