@1sat/cli 0.0.108 → 0.0.110

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,114 @@ 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
+ let currentRequest;
1432
+ const guarded = new Proxy(wallet, {
1433
+ get(target, property) {
1434
+ if (property === "createAction")
1435
+ return async (...args) => {
1436
+ const satoshis = args[0].outputs?.reduce((sum, output2) => sum + output2.satoshis, 0) ?? 0;
1437
+ if (!Number.isSafeInteger(satoshis) || satoshis <= 0)
1438
+ throw new Error("Invalid HTTP payment amount");
1439
+ if (approved === false)
1440
+ throw new PaymentRequired(satoshis);
1441
+ if (typeof approved === "number" && satoshis !== approved)
1442
+ throw new Error("Payment amount changed; review the new quote before retrying");
1443
+ if (submitted)
1444
+ throw new Error("Payment already submitted; reconcile wallet activity before retrying");
1445
+ submitted = true;
1446
+ const result = await target.createAction(...args);
1447
+ if (currentRequest?.retryCounter === 0)
1448
+ currentRequest.retryCounter = 1;
1449
+ return result;
1450
+ };
1451
+ if (property === "signAction")
1452
+ return async () => {
1453
+ throw new Error("Unexpected HTTP payment signing request");
1454
+ };
1455
+ const value = Reflect.get(target, property, target);
1456
+ return typeof value === "function" ? value.bind(target) : value;
1457
+ }
1458
+ });
1459
+ return {
1460
+ auth: new AuthFetch(guarded),
1461
+ prepareRequest: (config) => {
1462
+ currentRequest = config;
1463
+ },
1464
+ authorizePayment: (satoshis) => {
1465
+ approved = satoshis ?? true;
1466
+ }
1467
+ };
1468
+ }
1469
+ async function requestWithApproval(url, init, options, dependencies) {
1470
+ const send = () => {
1471
+ const config = {
1472
+ ...init,
1473
+ paymentRetryAttempts: 1,
1474
+ ...init.method === "GET" || init.method === "HEAD" ? {} : { retryCounter: 1 }
1475
+ };
1476
+ dependencies.prepareRequest?.(config);
1477
+ return dependencies.auth.fetch(url, config);
1478
+ };
1479
+ let res;
1480
+ try {
1481
+ res = await send();
1482
+ } catch (error) {
1483
+ if (error instanceof PaymentRequired) {
1484
+ res = new Response(null, {
1485
+ status: 402,
1486
+ headers: { "x-bsv-payment-satoshis-required": String(error.satoshis) }
1487
+ });
1488
+ } else {
1489
+ 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)`));
1490
+ if (!missingAuthentication)
1491
+ throw error;
1492
+ if (init.method !== "GET" && init.method !== "HEAD") {
1493
+ throw new Error("Authentication could not be verified. The request outcome may be unknown; verify activity before retrying.", { cause: error });
1494
+ }
1495
+ res = await dependencies.plainFetch(url, init);
1496
+ }
1497
+ }
1498
+ if (res.status !== 402)
1499
+ return res;
1500
+ const amount = res.headers.get("x-bsv-payment-satoshis-required");
1501
+ const value = amount !== null && /^\d{1,16}$/.test(amount) ? Number(amount) : Number.NaN;
1502
+ const satoshis = Number.isSafeInteger(value) ? value : undefined;
1503
+ const message = satoshis !== undefined ? `Pay ${satoshis} sats for ${init.method} ${url}?` : `Server requires payment (402) for ${init.method} ${url}. Continue?`;
1504
+ if (!options.yes) {
1505
+ if (!options.interactive) {
1506
+ return {
1507
+ error: "approval_required",
1508
+ status: 402,
1509
+ ok: false,
1510
+ message: "Payment requires approval. Review the payment and rerun with --yes to authorize it, or run in an interactive terminal.",
1511
+ ...satoshis !== undefined ? { satoshis } : {}
1512
+ };
1513
+ }
1514
+ if (!await dependencies.confirmPayment(message)) {
1515
+ throw new Error("Payment cancelled.");
1516
+ }
1517
+ }
1518
+ dependencies.authorizePayment?.(satoshis);
1519
+ return send();
1520
+ }
1521
+ var PaymentRequired;
1522
+ var init_authfetch_request = __esm(() => {
1523
+ PaymentRequired = class PaymentRequired extends Error {
1524
+ satoshis;
1525
+ constructor(satoshis) {
1526
+ super("Payment requires approval");
1527
+ this.satoshis = satoshis;
1528
+ }
1529
+ };
1530
+ });
1531
+
1426
1532
  // src/commands/authfetch.ts
1427
1533
  import { readFileSync as readFileSync5 } from "node:fs";
1428
- import { AuthFetch } from "@bsv/sdk";
1429
1534
  import { confirm, isCancel as isCancel2 } from "@clack/prompts";
1430
1535
  async function handleAuthfetchCommand(args, opts) {
1431
1536
  if (!args[0] || args[0] === "help" || args.includes("--help") || args.includes("-h")) {
@@ -1494,36 +1599,29 @@ Unknown or missing method: ${method ?? ""}`);
1494
1599
  chain: opts.chain
1495
1600
  });
1496
1601
  try {
1497
- const auth = new AuthFetch(walletResult.wallet);
1602
+ const { auth, authorizePayment, prepareRequest } = createApprovalAuth(walletResult.wallet);
1498
1603
  const init = {
1499
1604
  method,
1500
1605
  headers,
1501
1606
  ...body !== undefined ? { body } : {}
1502
1607
  };
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.");
1608
+ const res = await requestWithApproval(url, init, {
1609
+ yes: opts.yes,
1610
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
1611
+ }, {
1612
+ auth,
1613
+ authorizePayment,
1614
+ prepareRequest,
1615
+ plainFetch: fetch,
1616
+ confirmPayment: async (message) => {
1617
+ const ok = await confirm({ message });
1618
+ return !isCancel2(ok) && ok === true;
1525
1619
  }
1526
- res = await auth.fetch(url, init);
1620
+ });
1621
+ if ("error" in res) {
1622
+ output(res, opts);
1623
+ process.exitCode = 1;
1624
+ return;
1527
1625
  }
1528
1626
  const text = await res.text();
1529
1627
  let parsed = text;
@@ -1562,6 +1660,7 @@ var init_authfetch = __esm(() => {
1562
1660
  init_help();
1563
1661
  init_keys();
1564
1662
  init_output();
1663
+ init_authfetch_request();
1565
1664
  METHODS = new Set([
1566
1665
  "GET",
1567
1666
  "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.108",
3
+ "version": "0.0.110",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -0,0 +1,152 @@
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
+ let currentRequest: { retryCounter?: number } | undefined
14
+ const guarded = new Proxy(wallet, {
15
+ get(target, property) {
16
+ if (property === 'createAction')
17
+ return async (...args: Parameters<WalletInterface['createAction']>) => {
18
+ const satoshis =
19
+ args[0].outputs?.reduce(
20
+ (sum, output) => sum + output.satoshis,
21
+ 0,
22
+ ) ?? 0
23
+ if (!Number.isSafeInteger(satoshis) || satoshis <= 0)
24
+ throw new Error('Invalid HTTP payment amount')
25
+ if (approved === false) throw new PaymentRequired(satoshis)
26
+ if (typeof approved === 'number' && satoshis !== approved)
27
+ throw new Error(
28
+ 'Payment amount changed; review the new quote before retrying',
29
+ )
30
+ if (submitted)
31
+ throw new Error(
32
+ 'Payment already submitted; reconcile wallet activity before retrying',
33
+ )
34
+ submitted = true
35
+ const result = await target.createAction(...args)
36
+ // A verified 402 reached the approved wallet boundary. Permit its
37
+ // one paid send, but not stale-session replay of that send.
38
+ if (currentRequest?.retryCounter === 0)
39
+ currentRequest.retryCounter = 1
40
+ return result
41
+ }
42
+ if (property === 'signAction')
43
+ return async () => {
44
+ throw new Error('Unexpected HTTP payment signing request')
45
+ }
46
+ const value = Reflect.get(target, property, target)
47
+ return typeof value === 'function' ? value.bind(target) : value
48
+ },
49
+ })
50
+ return {
51
+ auth: new AuthFetch(guarded),
52
+ prepareRequest: (config: { retryCounter?: number }) => {
53
+ currentRequest = config
54
+ },
55
+ authorizePayment: (satoshis?: number) => {
56
+ approved = satoshis ?? true
57
+ },
58
+ }
59
+ }
60
+
61
+ export interface PaymentApprovalRequired {
62
+ error: 'approval_required'
63
+ status: 402
64
+ ok: false
65
+ message: string
66
+ satoshis?: number
67
+ }
68
+
69
+ /** Keep request replay and payment decisions separate from wallet setup/output. */
70
+ export async function requestWithApproval(
71
+ url: string,
72
+ init: RequestInit & { method: string },
73
+ options: { yes?: boolean; interactive: boolean },
74
+ dependencies: {
75
+ auth: Pick<AuthFetch, 'fetch'>
76
+ plainFetch: typeof fetch
77
+ confirmPayment: (message: string) => Promise<boolean>
78
+ authorizePayment?: (satoshis?: number) => void
79
+ prepareRequest?: (config: { retryCounter?: number }) => void
80
+ },
81
+ ): Promise<Response | PaymentApprovalRequired> {
82
+ const send = () => {
83
+ const config = {
84
+ ...init,
85
+ paymentRetryAttempts: 1,
86
+ ...(init.method === 'GET' || init.method === 'HEAD'
87
+ ? {}
88
+ : { retryCounter: 1 }),
89
+ }
90
+ dependencies.prepareRequest?.(config)
91
+ return dependencies.auth.fetch(url, config)
92
+ }
93
+ let res: Response
94
+ try {
95
+ res = await send()
96
+ } catch (error) {
97
+ if (error instanceof PaymentRequired) {
98
+ res = new Response(null, {
99
+ status: 402,
100
+ headers: { 'x-bsv-payment-satoshis-required': String(error.satoshis) },
101
+ })
102
+ } else {
103
+ // Match only the SDK's known unauthenticated-response error, not
104
+ // transport, signature verification, or other authentication failures.
105
+ const missingAuthentication =
106
+ error instanceof Error &&
107
+ /^Received HTTP \d{3}(?: [^\n]*)? from /.test(error.message) &&
108
+ (error.message.includes(
109
+ ` from ${url} without valid BSV authentication (missing headers: `,
110
+ ) ||
111
+ error.message.includes(
112
+ ` from ${url} without valid BSV authentication (response lacked required BSV auth headers)`,
113
+ ))
114
+ if (!missingAuthentication) throw error
115
+ if (init.method !== 'GET' && init.method !== 'HEAD') {
116
+ throw new Error(
117
+ 'Authentication could not be verified. The request outcome may be unknown; verify activity before retrying.',
118
+ { cause: error },
119
+ )
120
+ }
121
+ res = await dependencies.plainFetch(url, init)
122
+ }
123
+ }
124
+
125
+ if (res.status !== 402) return res
126
+
127
+ const amount = res.headers.get('x-bsv-payment-satoshis-required')
128
+ const value =
129
+ amount !== null && /^\d{1,16}$/.test(amount) ? Number(amount) : Number.NaN
130
+ const satoshis = Number.isSafeInteger(value) ? value : undefined
131
+ const message =
132
+ satoshis !== undefined
133
+ ? `Pay ${satoshis} sats for ${init.method} ${url}?`
134
+ : `Server requires payment (402) for ${init.method} ${url}. Continue?`
135
+ if (!options.yes) {
136
+ if (!options.interactive) {
137
+ return {
138
+ error: 'approval_required',
139
+ status: 402,
140
+ ok: false,
141
+ message:
142
+ 'Payment requires approval. Review the payment and rerun with --yes to authorize it, or run in an interactive terminal.',
143
+ ...(satoshis !== undefined ? { satoshis } : {}),
144
+ }
145
+ }
146
+ if (!(await dependencies.confirmPayment(message))) {
147
+ throw new Error('Payment cancelled.')
148
+ }
149
+ }
150
+ dependencies.authorizePayment?.(satoshis)
151
+ return send()
152
+ }
@@ -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,37 @@ export async function handleAuthfetchCommand(
107
107
  })
108
108
 
109
109
  try {
110
- const auth = new AuthFetch(walletResult.wallet)
110
+ const { auth, authorizePayment, prepareRequest } = createApprovalAuth(
111
+ walletResult.wallet,
112
+ )
111
113
  const init = {
112
114
  method,
113
115
  headers,
114
116
  ...(body !== undefined ? { body } : {}),
115
117
  }
116
118
 
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)
119
+ const res = await requestWithApproval(
120
+ url,
121
+ init,
122
+ {
123
+ yes: opts.yes,
124
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
125
+ },
126
+ {
127
+ auth,
128
+ authorizePayment,
129
+ prepareRequest,
130
+ plainFetch: fetch,
131
+ confirmPayment: async (message) => {
132
+ const ok = await confirm({ message })
133
+ return !isCancel(ok) && ok === true
134
+ },
135
+ },
136
+ )
137
+ if ('error' in res) {
138
+ output(res, opts)
139
+ process.exitCode = 1
140
+ return
148
141
  }
149
142
 
150
143
  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
  {