Skip to content
Open
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
88 changes: 80 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,8 @@ This library includes a `fetchWithL402` function to consume L402 protected resou
- url: the L402 protected URL
- fetchArgs: arguments are passed to the underlying `fetch()` function used to do the HTTP request
- options:
- wallet: any object that implements `sendPayment(paymentRequest)` and returns `{ preimage }`. Used to pay the L402 invoice.
- wallet: any object (e.g. a NWC client) that implements `payInvoice({ invoice })` and returns `{ preimage }`. Used to pay the L402 invoice.
- store: a key/value store object to persiste the l402 for each URL. The store must implement a `getItem()`/`setItem()` function as the browser's localStorage. By default a memory storage is used.
- headerKey: defaults to L402 but if you need to consume an old LSAT API set this to LSAT

##### Examples

Expand Down Expand Up @@ -220,15 +219,88 @@ await fetchWithL402(
.then(console.log);
```

### X402

Similar to L402 X402 is an open protocol for machine-to-machine payments built on the HTTP 402 Payment Required status code.
It enables APIs and resources to request payments inline, without prior registration or authentication.

This library includes a `fetchWithX402` function to consume X402-protected resources that support the lightning network.
(Note: X402 works also with other coins and network. This library supports X402 resources that accept Bitcoin on the lightning network)

#### fetchWithX402(url: string, fetchArgs, options)

- url: the X402 protected URL
- fetchArgs: arguments are passed to the underlying `fetch()` function used to do the HTTP request
- options:
- wallet: any object (e.g. a NWC client) that implements `payInvoice({ invoice })` and returns `{ preimage }`. Used to pay the X402 invoice.
- store: a key/value store object to persist the payment proof for each URL. The store must implement a `getItem()`/`setItem()` function as the browser's localStorage. By default a memory storage is used.

##### Examples

```js
import { fetchWithL402, NoStorage } from "@getalby/lightning-tools/l402";
import { fetchWithX402 } from "@getalby/lightning-tools/l402";

// do not store the tokens
await fetchWithL402(
"https://lsat-weather-api.getalby.repl.co/kigali",
// pass a wallet that implements payInvoice()
// the payment proof will not be stored by default. to reuse the proofs for subsequent requests provide a storage
await fetchWithX402(
"https://x402.albylabs.com/demo/quote",
{},
{ wallet: myWallet, store: window.localStorage },
)
.then((res) => res.json())
.then(console.log);
```

```js
import { fetchWithX402 } from "@getalby/lightning-tools/x402";
import { NostrWebLNProvider } from "@getalby/sdk";

// use a NWC provider as the wallet to do the payments
const nwc = new NostrWebLNProvider({
nostrWalletConnectUrl: loadNWCUrl(),
});

// this will fetch the resource and pay the invoice using the NWC wallet
await fetchWithX402(
"https://x402.albylabs.com/demo/quote",
{},
{ wallet: nwc },
)
.then((res) => res.json())
.then(console.log);
```

### fetch402

`fetch402` is a single function that transparently handles both L402 and X402 protected resources. Use it when you don't know or don't care which protocol the server uses — it will detect the protocol from the response headers and pay accordingly.

#### fetch402(url: string, fetchArgs, options)

- url: the protected URL
- fetchArgs: arguments are passed to the underlying `fetch()` function used to do the HTTP request
- options:
- wallet: any object that implements `payInvoice({ invoice })` and returns `{ preimage }`. Used to pay L402 and X402 invoices.
- store: a key/value store object to persist the payment proof for each URL. The store must implement a `getItem()`/`setItem()` function as the browser's localStorage. By default no storage is used - pass `window.localStorage` or a similar store to enable caching.

##### Examples


```js
import { fetch402 } from "@getalby/lightning-tools/l402";
import { NostrWebLNProvider } from "@getalby/sdk";

const nwc = new NostrWebLNProvider({
nostrWalletConnectUrl: "nostr+walletconnect://...",
});

// use a NWC wallet — works for both L402 and X402
await fetch402(
"https://example.com/protected-resource",
{},
{ store: new NoStorage() },
);
{ wallet: nwc, store: window.localStorage },
)
.then((res) => res.json())
.then(console.log);
```

### Basic invoice decoding
Expand Down
23 changes: 23 additions & 0 deletions examples/402.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { fetch402 } from "@getalby/lightning-tools/402";
import { NWCClient } from "@getalby/sdk";

// fetch402 works with both L402 and X402 endpoints —
// it detects the protocol from the server's response headers automatically.
const url = process.env.URL || "https://x402.albylabs.com/demo/quote";

const nostrWalletConnectUrl = process.env.NWC_URL;

if (!nostrWalletConnectUrl) {
throw new Error("Please set a NWC_URL env variable");
}

const nwc = new NWCClient({
nostrWalletConnectUrl,
});

fetch402(url, {}, { wallet: nwc })
.then((response) => response.json())
.then((data) => {
console.info(data);
nwc.close();
});
26 changes: 26 additions & 0 deletions examples/x402.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { fetchWithX402 } from "@getalby/lightning-tools/l402";
import { NostrWebLNProvider } from "@getalby/sdk";
import "websocket-polyfill";

const url = "https://x402.albylabs.com/demo/quote";

const nostrWalletConnectUrl = process.env.NWC_URL;

if (!nostrWalletConnectUrl) {
throw new Error("Please set a NWC_URL env variable");
}

const nwc = new NostrWebLNProvider({
nostrWalletConnectUrl,
});
await nwc.enable();
nwc.on("payInvoice", (response) => {
console.info(`payment response:`, response);
});

fetchWithX402(url, {}, { wallet: nwc })
.then((response) => response.json())
.then((data) => {
console.info(data);
nwc.close();
});
17 changes: 9 additions & 8 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFiles: [
"./setupJest.ts"
]
};
import type { Config } from "jest";

const config: Config = {
preset: "ts-jest",
testEnvironment: "node",
setupFiles: ["./setupJest.ts"],
};

export default config;
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,21 @@
"require": "./dist/cjs/fiat.cjs",
"types": "./dist/types/fiat.d.ts"
},
"./402": {
"import": "./dist/esm/402.js",
"require": "./dist/cjs/402.cjs",
"types": "./dist/types/402.d.ts"
},
"./l402": {
"import": "./dist/esm/l402.js",
"require": "./dist/cjs/l402.cjs",
"types": "./dist/types/l402.d.ts"
},
"./x402": {
"import": "./dist/esm/x402.js",
"require": "./dist/cjs/x402.cjs",
"types": "./dist/types/x402.d.ts"
},
"./lnurl": {
"import": "./dist/esm/lnurl.js",
"require": "./dist/cjs/lnurl.cjs",
Expand Down
4 changes: 3 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const entries = [
{ name: "index", input: "src/index.ts" },
{ name: "bolt11", input: "src/bolt11/index.ts" },
{ name: "fiat", input: "src/fiat/index.ts" },
{ name: "l402", input: "src/l402/index.ts" },
{ name: "402", input: "src/402/index.ts" },
{ name: "l402", input: "src/402/l402/index.ts" },
{ name: "x402", input: "src/402/x402/index.ts" },
{ name: "lnurl", input: "src/lnurl/index.ts" },
{ name: "podcasting2", input: "src/podcasting2/index.ts" },
];
Expand Down
132 changes: 132 additions & 0 deletions src/402/fetch402.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { KVStorage, NoStorage, Wallet } from "./utils";
import { parseL402 } from "./l402/utils";
import { buildX402PaymentSignature, X402Requirements } from "./x402/utils";
import { HEADER_KEY } from "./l402/l402";

const noStorage = new NoStorage();

export const fetch402 = async (
url: string,
fetchArgs: RequestInit,
options: {
wallet: Wallet;
store?: KVStorage;
},
) => {
const wallet = options.wallet;
const store = options.store || noStorage;
if (!fetchArgs) {
fetchArgs = {};
}
fetchArgs.cache = "no-store";
fetchArgs.mode = "cors";
const headers = new Headers(fetchArgs.headers ?? undefined);
fetchArgs.headers = headers;

// Check cache — detect protocol from stored data structure
const cachedRaw = store.getItem(url);
if (cachedRaw) {
const cached = JSON.parse(cachedRaw);
if (cached?.token && cached?.preimage) {
// L402 cached
headers.set(
"Authorization",
`${HEADER_KEY} ${cached.token}:${cached.preimage}`,
);
return await fetch(url, fetchArgs);
}
if (
cached?.scheme &&
cached?.network &&
cached?.invoice &&
cached?.requirements
) {
// X402 cached
headers.set(
"payment-signature",
buildX402PaymentSignature(
cached.scheme,
cached.network,
cached.invoice,
cached.requirements,
),
);
return await fetch(url, fetchArgs);
}
}

// Initial request — advertise L402 support
headers.set("Accept-Authenticate", HEADER_KEY);
const initResp = await fetch(url, fetchArgs);

const l402Header = initResp.headers.get("www-authenticate");
if (l402Header) {
const details = parseL402(l402Header);
const token = details.token || details.macaroon;
const invoice = details.invoice;

const invResp = await wallet.payInvoice!({ invoice });

store.setItem(url, JSON.stringify({ token, preimage: invResp.preimage }));

headers.set("Authorization", `${HEADER_KEY} ${token}:${invResp.preimage}`);

return await fetch(url, fetchArgs);
}

const x402Header = initResp.headers.get("PAYMENT-REQUIRED");
if (x402Header) {
let parsed: { accepts?: unknown[] };
try {
parsed = JSON.parse(decodeURIComponent(escape(atob(x402Header))));
} catch (_) {
throw new Error(
"x402: invalid PAYMENT-REQUIRED header (not valid base64-encoded JSON)",
);
}

if (!Array.isArray(parsed.accepts) || parsed.accepts.length === 0) {
throw new Error(
"x402: PAYMENT-REQUIRED header contains no payment options",
);
}

const requirements = (parsed.accepts as X402Requirements[]).find((e) => {
return e.extra?.paymentMethod === "lightning";
});
if (!requirements) {
throw new Error(
"x402: unsupported x402 network, only lightning networks are supported",
);
}
if (!requirements.extra?.invoice) {
throw new Error("x402: payment requirements missing lightning invoice");
}

const invoice = requirements.extra.invoice;
await wallet.payInvoice!({ invoice });

store.setItem(
url,
JSON.stringify({
scheme: requirements.scheme,
network: requirements.network,
invoice,
requirements,
}),
);

headers.set(
"payment-signature",
buildX402PaymentSignature(
requirements.scheme,
requirements.network,
invoice,
requirements,
),
);
return await fetch(url, fetchArgs);
}

return initResp;
};
2 changes: 2 additions & 0 deletions src/402/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./fetch402";
export * from "./utils";
File renamed without changes.
Loading
Loading