diff --git a/package.json b/package.json index 5cbd7c6..cb40b60 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@getalby/cli", "description": "CLI for Nostr Wallet Connect (NIP-47) with a few additional useful lightning tools", "repository": "https://github.com/getAlby/cli.git", - "version": "0.8.0", + "version": "0.9.0", "type": "module", "main": "build/index.js", "bin": { @@ -36,8 +36,8 @@ "node": ">=20" }, "dependencies": { - "@getalby/lightning-tools": "^8.1.1", - "@getalby/sdk": "^8.0.1", + "@getalby/lightning-tools": "^8.2.0", + "@getalby/sdk": "^8.0.3", "@lendasat/lendaswap-sdk-pure": "^0.2.38", "@noble/hashes": "^2.0.1", "commander": "^14.0.3", diff --git a/src/commands/fetch.ts b/src/commands/fetch.ts index f7042d6..4d7c60c 100644 --- a/src/commands/fetch.ts +++ b/src/commands/fetch.ts @@ -25,6 +25,38 @@ function parseMaxAmountSats(value: string): number { return sats; } +/** + * Parse and validate the `--credentials` JSON. It must be an object with string + * `header` and `value` fields, matching the reusable credential emitted in a + * previous fetch's `payment.credentials`. Rejects anything else so a malformed + * credential fails loudly instead of being silently ignored by the fetch helper. + */ +function parseCredentials(value: string): { header: string; value: string } { + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + throw new InvalidArgumentError( + "--credentials must be valid JSON (e.g. '{\"header\":\"Authorization\",\"value\":\"L402 ...\"}')", + ); + } + if ( + typeof parsed !== "object" || + parsed === null || + typeof (parsed as Record).header !== "string" || + typeof (parsed as Record).value !== "string" + ) { + throw new InvalidArgumentError( + '--credentials must be a JSON object with string "header" and "value" fields', + ); + } + const { header, value: headerValue } = parsed as { + header: string; + value: string; + }; + return { header, value: headerValue }; +} + export function registerFetch402Command(program: Command) { program .command("fetch") @@ -53,10 +85,20 @@ export function registerFetch402Command(program: Command) { "--network ", "Rail for --max-amount — currently must be lightning", ) + .option( + "--credentials ", + "Reusable payment credential from a previous fetch " + + '(JSON: {"header":"...","value":"..."}). When set, the request is ' + + "authorized with it and never pays again — use it to authorize " + + "follow-up requests (e.g. polling) without re-paying.", + ) .addHelpText( "after", "\nExample:\n" + - ' $ npx @getalby/cli fetch "https://example.com/api" --max-amount 1000 --currency BTC --unit sats --network lightning\n', + ' $ npx @getalby/cli fetch "https://example.com/api" --max-amount 1000 --currency BTC --unit sats --network lightning\n' + + "\nA successful response includes a `payment` object with the fees paid and a\n" + + "reusable `credentials` value. Reuse it to avoid paying again:\n" + + ' $ npx @getalby/cli fetch "https://example.com/api" --credentials \'{"header":"Authorization","value":"L402 ..."}\'\n', ) .action(async (url, options) => { await handleError(async () => { @@ -91,6 +133,9 @@ export function registerFetch402Command(program: Command) { // --unit is restricted to sats above, so --max-amount is already the // sats cap. When omitted, the tool applies its default. maxAmountSats: options.maxAmount, + credentials: options.credentials + ? parseCredentials(options.credentials) + : undefined, }); output(result); }); diff --git a/src/index.ts b/src/index.ts index e8aae48..80d0378 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,7 @@ program " $ npx @getalby/cli pay alice@getalby.com --amount 5 --currency USD --network lightning\n" + ' $ npx @getalby/cli receive --amount 2100 --currency BTC --unit sats --network lightning --description "Coffee"', ) - .version("0.8.0") + .version("0.9.0") .configureHelp({ showGlobalOptions: true }) .option( "-w, --wallet-name ", diff --git a/src/tools/lightning/fetch.ts b/src/tools/lightning/fetch.ts index c61eda1..bc7aeb9 100644 --- a/src/tools/lightning/fetch.ts +++ b/src/tools/lightning/fetch.ts @@ -1,4 +1,7 @@ -import { fetch402 as fetch402Lib } from "@getalby/lightning-tools/402"; +import { + fetch402 as fetch402Lib, + type PaymentCredentials, +} from "@getalby/lightning-tools/402"; import { NWCClient } from "@getalby/sdk"; const DEFAULT_MAX_AMOUNT_SATS = 5000; @@ -9,6 +12,12 @@ export interface Fetch402Params { body?: string; headers?: Record; maxAmountSats?: number; + /** + * A reusable credential returned by a previous fetch. When provided the + * request is authorized with it and NEVER pays again - use it to authorize + * follow-up requests (e.g. polling a long-running job) without re-paying. + */ + credentials?: PaymentCredentials; } export async function fetch402(client: NWCClient, params: Fetch402Params) { @@ -39,6 +48,7 @@ export async function fetch402(client: NWCClient, params: Fetch402Params) { const result = await fetch402Lib(params.url, requestOptions, { wallet: client, maxAmount: maxAmountSats, + credentials: params.credentials, }); const responseContent = await result.text(); @@ -50,5 +60,11 @@ export async function fetch402(client: NWCClient, params: Fetch402Params) { return { content: responseContent, + // Payment metadata attached by the 402 helper: whether a payment was made, + // the amount, routing fees (feesPaid, in millisatoshis), and the reusable + // credential. Pass `credentials` back via --credentials on a follow-up + // request to authorize it without paying again. Absent when no 402 payment + // was involved (e.g. an already-open resource). + payment: result.payment, }; } diff --git a/yarn.lock b/yarn.lock index e962713..041c965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,10 +78,15 @@ resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-8.1.1.tgz#4bf8be9f30dd76f7dd082262e862e63c16591fae" integrity sha512-gB+3fvTGz9bbl31JNgKMWqG4dZWgCxSB51fYn+F4tZ3x2h5oI/yHjvb6W9GBo2Zscr5gMP64gs0LlzKYoETlvQ== -"@getalby/sdk@^8.0.1": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-8.0.1.tgz#6202ad96692e54e37bd7b3d0b530b7b1bb03f9b6" - integrity sha512-DaRmP5/0z0Xh+UqV4UgCLeYv05SkGsq+BS4sQr5dTI342IKz57T/5YaRGcCipmYPWXL88HObaivd8oJgf9LP3A== +"@getalby/lightning-tools@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-8.2.0.tgz#7bcd7c24149f5dfe59e4660aebcda22637d1f207" + integrity sha512-eL+cnHyzeUARKVzNRuFBRBppAU5RBJOLjhFVzSWt8hDibRzL5+e4hq8I79+RGHfrWWMUDv382Jx8ewOazrBj7w== + +"@getalby/sdk@^8.0.3": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-8.0.3.tgz#dd59ce94682c1c041fa90b42c16e959aa2d59f03" + integrity sha512-vPEogAWwLHbL55COeXrRN7yRBXMDGDxX1M2A+o3Lqw60FFng6fa9FK9sw8ptdILMD1dQZq8FzPXuabQ7seoDBA== dependencies: "@getalby/lightning-tools" "^8.1.1" nostr-tools "^2.23.3"