@1sat/cli 0.0.106 → 0.0.109

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/README.md CHANGED
@@ -331,3 +331,20 @@ The CLI is pure Bun with no framework. Arg parsing is manual (`src/args.ts`). Bu
331
331
  ## License
332
332
 
333
333
  MIT
334
+
335
+ ### Authenticated HTTP requests
336
+
337
+ `1sat authfetch <method> <url> [--body <json|@file>] [--header 'K: V']`
338
+ uses the SDK's BRC-104 AuthFetch with CLI wallet keys. If the SDK reports
339
+ missing response authentication headers, only GET and HEAD may retry using
340
+ plain fetch for public reads. Other methods fail with an unknown-outcome
341
+ warning: verify activity before retrying, since the server may already have
342
+ executed the request.
343
+
344
+ A 402 response requires interactive payment confirmation or explicit `--yes`.
345
+ Without `--yes` and a terminal on both stdin and stdout, the command returns
346
+ `approval_required` with exit status 1 and makes no payment retry. Use `--json`
347
+ for structured output; `satoshis` is included only when the public payment
348
+ header is a nonnegative safe integer of at most 16 decimal digits. Review the
349
+ payment before rerunning with `--yes`; this authorizes the existing SDK payment
350
+ flow. It does not impose a spending cap. `--quiet` suppresses output as usual.
package/dist/cli.js CHANGED
@@ -1378,7 +1378,7 @@ var init_help = __esm(() => {
1378
1378
  {
1379
1379
  group: "Advanced",
1380
1380
  name: "authfetch",
1381
- description: "HTTP request with BRC-104 AuthFetch using CLI wallet keys. On 402, confirms payment unless --yes.",
1381
+ description: "HTTP request with BRC-104 AuthFetch using CLI wallet keys. Only GET/HEAD retry public routes without auth. On 402, requires --yes or interactive confirmation; noninteractive calls return approval_required and exit nonzero.",
1382
1382
  positional: "<method> <url>",
1383
1383
  args: [
1384
1384
  {
@@ -1423,9 +1423,101 @@ var init_help = __esm(() => {
1423
1423
  ];
1424
1424
  });
1425
1425
 
1426
+ // src/commands/authfetch-request.ts
1427
+ import { AuthFetch } from "@bsv/sdk";
1428
+ function createApprovalAuth(wallet) {
1429
+ let approved = false;
1430
+ let submitted = false;
1431
+ const guarded = new Proxy(wallet, {
1432
+ get(target, property) {
1433
+ if (property === "createAction")
1434
+ return async (...args) => {
1435
+ const satoshis = args[0].outputs?.reduce((sum, output2) => sum + output2.satoshis, 0) ?? 0;
1436
+ if (!Number.isSafeInteger(satoshis) || satoshis <= 0)
1437
+ throw new Error("Invalid HTTP payment amount");
1438
+ if (approved === false)
1439
+ throw new PaymentRequired(satoshis);
1440
+ if (typeof approved === "number" && satoshis !== approved)
1441
+ throw new Error("Payment amount changed; review the new quote before retrying");
1442
+ if (submitted)
1443
+ throw new Error("Payment already submitted; reconcile wallet activity before retrying");
1444
+ submitted = true;
1445
+ return target.createAction(...args);
1446
+ };
1447
+ if (property === "signAction")
1448
+ return async () => {
1449
+ throw new Error("Unexpected HTTP payment signing request");
1450
+ };
1451
+ const value = Reflect.get(target, property, target);
1452
+ return typeof value === "function" ? value.bind(target) : value;
1453
+ }
1454
+ });
1455
+ return {
1456
+ auth: new AuthFetch(guarded),
1457
+ authorizePayment: (satoshis) => {
1458
+ approved = satoshis ?? true;
1459
+ }
1460
+ };
1461
+ }
1462
+ async function requestWithApproval(url, init, options, dependencies) {
1463
+ let res;
1464
+ try {
1465
+ res = await dependencies.auth.fetch(url, {
1466
+ ...init,
1467
+ paymentRetryAttempts: 1
1468
+ });
1469
+ } catch (error) {
1470
+ if (error instanceof PaymentRequired) {
1471
+ res = new Response(null, {
1472
+ status: 402,
1473
+ headers: { "x-bsv-payment-satoshis-required": String(error.satoshis) }
1474
+ });
1475
+ } else {
1476
+ const missingAuthentication = error instanceof Error && /^Received HTTP \d{3}(?: [^\n]*)? from /.test(error.message) && (error.message.includes(` from ${url} without valid BSV authentication (missing headers: `) || error.message.includes(` from ${url} without valid BSV authentication (response lacked required BSV auth headers)`));
1477
+ if (!missingAuthentication)
1478
+ throw error;
1479
+ if (init.method !== "GET" && init.method !== "HEAD") {
1480
+ throw new Error("Authentication could not be verified. The request outcome may be unknown; verify activity before retrying.", { cause: error });
1481
+ }
1482
+ res = await dependencies.plainFetch(url, init);
1483
+ }
1484
+ }
1485
+ if (res.status !== 402)
1486
+ return res;
1487
+ const amount = res.headers.get("x-bsv-payment-satoshis-required");
1488
+ const value = amount !== null && /^\d{1,16}$/.test(amount) ? Number(amount) : Number.NaN;
1489
+ const satoshis = Number.isSafeInteger(value) ? value : undefined;
1490
+ const message = satoshis !== undefined ? `Pay ${satoshis} sats for ${init.method} ${url}?` : `Server requires payment (402) for ${init.method} ${url}. Continue?`;
1491
+ if (!options.yes) {
1492
+ if (!options.interactive) {
1493
+ return {
1494
+ error: "approval_required",
1495
+ status: 402,
1496
+ ok: false,
1497
+ message: "Payment requires approval. Review the payment and rerun with --yes to authorize it, or run in an interactive terminal.",
1498
+ ...satoshis !== undefined ? { satoshis } : {}
1499
+ };
1500
+ }
1501
+ if (!await dependencies.confirmPayment(message)) {
1502
+ throw new Error("Payment cancelled.");
1503
+ }
1504
+ }
1505
+ dependencies.authorizePayment?.(satoshis);
1506
+ return dependencies.auth.fetch(url, { ...init, paymentRetryAttempts: 1 });
1507
+ }
1508
+ var PaymentRequired;
1509
+ var init_authfetch_request = __esm(() => {
1510
+ PaymentRequired = class PaymentRequired extends Error {
1511
+ satoshis;
1512
+ constructor(satoshis) {
1513
+ super("Payment requires approval");
1514
+ this.satoshis = satoshis;
1515
+ }
1516
+ };
1517
+ });
1518
+
1426
1519
  // src/commands/authfetch.ts
1427
1520
  import { readFileSync as readFileSync5 } from "node:fs";
1428
- import { AuthFetch } from "@bsv/sdk";
1429
1521
  import { confirm, isCancel as isCancel2 } from "@clack/prompts";
1430
1522
  async function handleAuthfetchCommand(args, opts) {
1431
1523
  if (!args[0] || args[0] === "help" || args.includes("--help") || args.includes("-h")) {
@@ -1494,36 +1586,28 @@ Unknown or missing method: ${method ?? ""}`);
1494
1586
  chain: opts.chain
1495
1587
  });
1496
1588
  try {
1497
- const auth = new AuthFetch(walletResult.wallet);
1589
+ const { auth, authorizePayment } = createApprovalAuth(walletResult.wallet);
1498
1590
  const init = {
1499
1591
  method,
1500
1592
  headers,
1501
1593
  ...body !== undefined ? { body } : {}
1502
1594
  };
1503
- let res;
1504
- try {
1505
- res = await auth.fetch(url, {
1506
- ...init,
1507
- paymentRetryAttempts: 0
1508
- });
1509
- } catch (err) {
1510
- const msg = err instanceof Error ? err.message : String(err);
1511
- if (/without valid BSV authentication/i.test(msg)) {
1512
- res = await fetch(url, init);
1513
- } else {
1514
- throw err;
1515
- }
1516
- }
1517
- if (res.status === 402) {
1518
- const satsHeader = res.headers.get("x-bsv-payment-satoshis-required");
1519
- const sats = satsHeader ? Number(satsHeader) : undefined;
1520
- const payMsg = sats != null && Number.isFinite(sats) ? `Pay ${sats} sats for ${method} ${url}?` : `Server requires payment (402) for ${method} ${url}. Continue?`;
1521
- if (!opts.yes) {
1522
- const ok = await confirm({ message: payMsg });
1523
- if (isCancel2(ok) || !ok)
1524
- fatal("Payment cancelled.");
1595
+ const res = await requestWithApproval(url, init, {
1596
+ yes: opts.yes,
1597
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
1598
+ }, {
1599
+ auth,
1600
+ authorizePayment,
1601
+ plainFetch: fetch,
1602
+ confirmPayment: async (message) => {
1603
+ const ok = await confirm({ message });
1604
+ return !isCancel2(ok) && ok === true;
1525
1605
  }
1526
- res = await auth.fetch(url, init);
1606
+ });
1607
+ if ("error" in res) {
1608
+ output(res, opts);
1609
+ process.exitCode = 1;
1610
+ return;
1527
1611
  }
1528
1612
  const text = await res.text();
1529
1613
  let parsed = text;
@@ -1562,6 +1646,7 @@ var init_authfetch = __esm(() => {
1562
1646
  init_help();
1563
1647
  init_keys();
1564
1648
  init_output();
1649
+ init_authfetch_request();
1565
1650
  METHODS = new Set([
1566
1651
  "GET",
1567
1652
  "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.106",
3
+ "version": "0.0.109",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -27,11 +27,11 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
- "@1sat/actions": "0.0.205",
30
+ "@1sat/actions": "0.0.207",
31
31
  "@1sat/client": "0.0.51",
32
32
  "@1sat/types": "0.0.41",
33
- "@1sat/wallet-node": "0.0.72",
34
- "@1sat/wallet-server": "0.0.47",
33
+ "@1sat/wallet-node": "0.0.73",
34
+ "@1sat/wallet-server": "0.0.48",
35
35
  "@bsv/sdk": "^2.1.9",
36
36
  "@bsv/wallet-toolbox": "npm:@bopen-io/wallet-toolbox@2.6.2-brc153.5",
37
37
  "chalk": "^5.0.0",
@@ -0,0 +1,134 @@
1
+ import { AuthFetch, type WalletInterface } from '@bsv/sdk'
2
+
3
+ class PaymentRequired extends Error {
4
+ constructor(readonly satoshis: number) {
5
+ super('Payment requires approval')
6
+ }
7
+ }
8
+
9
+ /** Enforce approval at the wallet boundary; SDK retry=0 does not disable payment. */
10
+ export function createApprovalAuth(wallet: WalletInterface) {
11
+ let approved: false | number | true = false
12
+ let submitted = false
13
+ const guarded = new Proxy(wallet, {
14
+ get(target, property) {
15
+ if (property === 'createAction')
16
+ return async (...args: Parameters<WalletInterface['createAction']>) => {
17
+ const satoshis =
18
+ args[0].outputs?.reduce(
19
+ (sum, output) => sum + output.satoshis,
20
+ 0,
21
+ ) ?? 0
22
+ if (!Number.isSafeInteger(satoshis) || satoshis <= 0)
23
+ throw new Error('Invalid HTTP payment amount')
24
+ if (approved === false) throw new PaymentRequired(satoshis)
25
+ if (typeof approved === 'number' && satoshis !== approved)
26
+ throw new Error(
27
+ 'Payment amount changed; review the new quote before retrying',
28
+ )
29
+ if (submitted)
30
+ throw new Error(
31
+ 'Payment already submitted; reconcile wallet activity before retrying',
32
+ )
33
+ submitted = true
34
+ return target.createAction(...args)
35
+ }
36
+ if (property === 'signAction')
37
+ return async () => {
38
+ throw new Error('Unexpected HTTP payment signing request')
39
+ }
40
+ const value = Reflect.get(target, property, target)
41
+ return typeof value === 'function' ? value.bind(target) : value
42
+ },
43
+ })
44
+ return {
45
+ auth: new AuthFetch(guarded),
46
+ authorizePayment: (satoshis?: number) => {
47
+ approved = satoshis ?? true
48
+ },
49
+ }
50
+ }
51
+
52
+ export interface PaymentApprovalRequired {
53
+ error: 'approval_required'
54
+ status: 402
55
+ ok: false
56
+ message: string
57
+ satoshis?: number
58
+ }
59
+
60
+ /** Keep request replay and payment decisions separate from wallet setup/output. */
61
+ export async function requestWithApproval(
62
+ url: string,
63
+ init: RequestInit & { method: string },
64
+ options: { yes?: boolean; interactive: boolean },
65
+ dependencies: {
66
+ auth: Pick<AuthFetch, 'fetch'>
67
+ plainFetch: typeof fetch
68
+ confirmPayment: (message: string) => Promise<boolean>
69
+ authorizePayment?: (satoshis?: number) => void
70
+ },
71
+ ): Promise<Response | PaymentApprovalRequired> {
72
+ let res: Response
73
+ try {
74
+ res = await dependencies.auth.fetch(url, {
75
+ ...init,
76
+ paymentRetryAttempts: 1,
77
+ })
78
+ } catch (error) {
79
+ if (error instanceof PaymentRequired) {
80
+ res = new Response(null, {
81
+ status: 402,
82
+ headers: { 'x-bsv-payment-satoshis-required': String(error.satoshis) },
83
+ })
84
+ } else {
85
+ // Match only the SDK's known unauthenticated-response error, not
86
+ // transport, signature verification, or other authentication failures.
87
+ const missingAuthentication =
88
+ error instanceof Error &&
89
+ /^Received HTTP \d{3}(?: [^\n]*)? from /.test(error.message) &&
90
+ (error.message.includes(
91
+ ` from ${url} without valid BSV authentication (missing headers: `,
92
+ ) ||
93
+ error.message.includes(
94
+ ` from ${url} without valid BSV authentication (response lacked required BSV auth headers)`,
95
+ ))
96
+ if (!missingAuthentication) throw error
97
+ if (init.method !== 'GET' && init.method !== 'HEAD') {
98
+ throw new Error(
99
+ 'Authentication could not be verified. The request outcome may be unknown; verify activity before retrying.',
100
+ { cause: error },
101
+ )
102
+ }
103
+ res = await dependencies.plainFetch(url, init)
104
+ }
105
+ }
106
+
107
+ if (res.status !== 402) return res
108
+
109
+ const amount = res.headers.get('x-bsv-payment-satoshis-required')
110
+ const value =
111
+ amount !== null && /^\d{1,16}$/.test(amount) ? Number(amount) : Number.NaN
112
+ const satoshis = Number.isSafeInteger(value) ? value : undefined
113
+ const message =
114
+ satoshis !== undefined
115
+ ? `Pay ${satoshis} sats for ${init.method} ${url}?`
116
+ : `Server requires payment (402) for ${init.method} ${url}. Continue?`
117
+ if (!options.yes) {
118
+ if (!options.interactive) {
119
+ return {
120
+ error: 'approval_required',
121
+ status: 402,
122
+ ok: false,
123
+ message:
124
+ 'Payment requires approval. Review the payment and rerun with --yes to authorize it, or run in an interactive terminal.',
125
+ ...(satoshis !== undefined ? { satoshis } : {}),
126
+ }
127
+ }
128
+ if (!(await dependencies.confirmPayment(message))) {
129
+ throw new Error('Payment cancelled.')
130
+ }
131
+ }
132
+ dependencies.authorizePayment?.(satoshis)
133
+ return dependencies.auth.fetch(url, { ...init, paymentRetryAttempts: 1 })
134
+ }
@@ -3,18 +3,18 @@
3
3
  *
4
4
  * 1sat authfetch <method> <url> [--body <json|@file>] [--header 'K: V']...
5
5
  *
6
- * Always authenticates. On 402 Payment Required, prompts unless --yes, then
7
- * AuthFetch pays and retries.
6
+ * Authenticates first; only GET/HEAD may retry public routes without auth.
7
+ * Payment requires --yes or interactive confirmation.
8
8
  */
9
9
 
10
10
  import { readFileSync } from 'node:fs'
11
- import { AuthFetch } from '@bsv/sdk'
12
11
  import { confirm, isCancel } from '@clack/prompts'
13
12
  import type { GlobalFlags } from '../args.js'
14
13
  import { loadContext } from '../context.js'
15
14
  import { printCommandHelp } from '../help.js'
16
15
  import { loadKey } from '../keys.js'
17
16
  import { fatal, output } from '../output.js'
17
+ import { createApprovalAuth, requestWithApproval } from './authfetch-request.js'
18
18
 
19
19
  const METHODS = new Set([
20
20
  'GET',
@@ -107,44 +107,34 @@ export async function handleAuthfetchCommand(
107
107
  })
108
108
 
109
109
  try {
110
- const auth = new AuthFetch(walletResult.wallet)
110
+ const { auth, authorizePayment } = createApprovalAuth(walletResult.wallet)
111
111
  const init = {
112
112
  method,
113
113
  headers,
114
114
  ...(body !== undefined ? { body } : {}),
115
115
  }
116
116
 
117
- let res: Response
118
- try {
119
- // Authenticate but do not auto-pay on first try (so we can confirm).
120
- res = await auth.fetch(url, {
121
- ...init,
122
- paymentRetryAttempts: 0,
123
- })
124
- } catch (err) {
125
- // Public routes may return 200 without BSV auth headers; AuthFetch rejects those.
126
- const msg = err instanceof Error ? err.message : String(err)
127
- if (/without valid BSV authentication/i.test(msg)) {
128
- res = await fetch(url, init)
129
- } else {
130
- throw err
131
- }
132
- }
133
-
134
- if (res.status === 402) {
135
- const satsHeader = res.headers.get('x-bsv-payment-satoshis-required')
136
- const sats = satsHeader ? Number(satsHeader) : undefined
137
- const payMsg =
138
- sats != null && Number.isFinite(sats)
139
- ? `Pay ${sats} sats for ${method} ${url}?`
140
- : `Server requires payment (402) for ${method} ${url}. Continue?`
141
-
142
- if (!opts.yes) {
143
- const ok = await confirm({ message: payMsg })
144
- if (isCancel(ok) || !ok) fatal('Payment cancelled.')
145
- }
146
-
147
- res = await auth.fetch(url, init)
117
+ const res = await requestWithApproval(
118
+ url,
119
+ init,
120
+ {
121
+ yes: opts.yes,
122
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
123
+ },
124
+ {
125
+ auth,
126
+ authorizePayment,
127
+ plainFetch: fetch,
128
+ confirmPayment: async (message) => {
129
+ const ok = await confirm({ message })
130
+ return !isCancel(ok) && ok === true
131
+ },
132
+ },
133
+ )
134
+ if ('error' in res) {
135
+ output(res, opts)
136
+ process.exitCode = 1
137
+ return
148
138
  }
149
139
 
150
140
  const text = await res.text()
package/src/help.ts CHANGED
@@ -763,7 +763,7 @@ export const COMMANDS: CommandSpec[] = [
763
763
  group: 'Advanced',
764
764
  name: 'authfetch',
765
765
  description:
766
- 'HTTP request with BRC-104 AuthFetch using CLI wallet keys. On 402, confirms payment unless --yes.',
766
+ 'HTTP request with BRC-104 AuthFetch using CLI wallet keys. Only GET/HEAD retry public routes without auth. On 402, requires --yes or interactive confirmation; noninteractive calls return approval_required and exit nonzero.',
767
767
  positional: '<method> <url>',
768
768
  args: [
769
769
  {