Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
47 changes: 46 additions & 1 deletion src/commands/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).header !== "string" ||
typeof (parsed as Record<string, unknown>).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")
Expand Down Expand Up @@ -53,10 +85,20 @@ export function registerFetch402Command(program: Command) {
"--network <name>",
"Rail for --max-amount — currently must be lightning",
)
.option(
"--credentials <json>",
"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 () => {
Expand Down Expand Up @@ -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);
});
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>",
Expand Down
18 changes: 17 additions & 1 deletion src/tools/lightning/fetch.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,6 +12,12 @@ export interface Fetch402Params {
body?: string;
headers?: Record<string, string>;
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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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,
};
}
13 changes: 9 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading