Skip to content

Commit 238face

Browse files
realbubclaude
andcommitted
v1.0.0: migrate to wallet-scoped L402 API
Update to @lnbot/sdk v1.0.0 where L402 operations are wallet-scoped. Both paywall() and client() now require a walletId option to access ln.wallet(walletId).l402 instead of the removed ln.l402. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4ae54d7 commit 238face

8 files changed

Lines changed: 136 additions & 109 deletions

File tree

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lnbot/l402",
3-
"version": "0.2.0",
3+
"version": "1.0.0",
44
"description": "L402 Lightning payment middleware for Express.js — paywall any API in one line",
55
"type": "module",
66
"exports": {
@@ -46,11 +46,11 @@
4646
"test:watch": "vitest"
4747
},
4848
"peerDependencies": {
49-
"@lnbot/sdk": ">=0.1.0",
49+
"@lnbot/sdk": ">=1.0.0",
5050
"express": ">=4.0.0"
5151
},
5252
"devDependencies": {
53-
"@lnbot/sdk": "^0.5.0",
53+
"@lnbot/sdk": "^1.0.0",
5454
"@types/express": "^5",
5555
"express": "^5",
5656
"tsup": "^8",

src/client/fetch.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ export interface L402Client {
2828
/**
2929
* Create an L402-aware HTTP client.
3030
*
31-
* One SDK call: `ln.l402.pay()` — pays the invoice and returns the Authorization token.
31+
* One SDK call: `wallet.l402.pay()` — pays the invoice and returns the Authorization token.
3232
*/
33-
export function client(ln: LnBot, options: L402ClientOptions = {}): L402Client {
33+
export function client(ln: LnBot, options: L402ClientOptions): L402Client {
34+
const wallet = ln.wallet(options.walletId);
3435
const store = resolveStore(options.store);
3536
const budget = new Budget(options);
3637
const maxPrice = options.maxPrice ?? 1000;
@@ -81,7 +82,7 @@ export function client(ln: LnBot, options: L402ClientOptions = {}): L402Client {
8182
budget.check(price);
8283

8384
// Step 5: Pay via SDK
84-
const payment = await ln.l402.pay({ wwwAuthenticate: wwwAuth });
85+
const payment = await wallet.l402.pay({ wwwAuthenticate: wwwAuth });
8586

8687
if (payment.status === "failed") {
8788
throw new L402PaymentFailedError("L402 payment failed");

src/server/middleware.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ import { resolvePrice } from "./pricing.js";
77
* Express middleware factory that protects routes behind an L402 paywall.
88
*
99
* Two SDK calls:
10-
* - `ln.l402.verify()` — check an incoming Authorization header
11-
* - `ln.l402.createChallenge()` — mint a new invoice + macaroon challenge
10+
* - `wallet.l402.verify()` — check an incoming Authorization header
11+
* - `wallet.l402.createChallenge()` — mint a new invoice + macaroon challenge
1212
*/
1313
export function paywall(ln: LnBot, options: L402PaywallOptions) {
1414
return async (req: Request, res: Response, next: NextFunction) => {
15+
const wallet = ln.wallet(options.walletId);
16+
1517
// Step 1: Check for existing L402 Authorization header
1618
const authHeader = req.headers["authorization"];
1719

1820
if (authHeader && authHeader.startsWith("L402 ")) {
1921
// Step 2: Verify via SDK (stateless — checks signature, preimage, caveats)
2022
try {
21-
const result = await ln.l402.verify({ authorization: authHeader });
23+
const result = await wallet.l402.verify({ authorization: authHeader });
2224

2325
if (result.valid) {
2426
req.l402 = {
@@ -36,7 +38,7 @@ export function paywall(ln: LnBot, options: L402PaywallOptions) {
3638
const price = await resolvePrice(options.price, req);
3739

3840
try {
39-
const challenge = await ln.l402.createChallenge({
41+
const challenge = await wallet.l402.createChallenge({
4042
amount: price,
4143
description: options.description,
4244
expirySeconds: options.expirySeconds,

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ declare global {
1616

1717
/** Options for the `l402.paywall()` middleware. */
1818
export interface L402PaywallOptions {
19+
/** The wallet ID to use for L402 operations. */
20+
walletId: string;
1921
/** Price in satoshis — fixed number or async function receiving the request. */
2022
price: number | ((req: Request) => number | Promise<number>);
2123
/** Invoice memo / description. */
@@ -28,6 +30,8 @@ export interface L402PaywallOptions {
2830

2931
/** Options for the `l402.client()` factory. */
3032
export interface L402ClientOptions {
33+
/** The wallet ID to use for L402 operations. */
34+
walletId: string;
3135
/** Max sats to pay for a single request (default: 1000). */
3236
maxPrice?: number;
3337
/** Total budget in sats for the period. */

tests/client.test.ts

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ import { L402BudgetExceededError } from "../src/errors.js";
77
// ── Mock LnBot SDK ──
88

99
function createMockLn() {
10+
const l402 = {
11+
createChallenge: vi.fn(),
12+
verify: vi.fn(),
13+
pay: vi.fn(),
14+
};
1015
return {
11-
l402: {
12-
createChallenge: vi.fn(),
13-
verify: vi.fn(),
14-
pay: vi.fn(),
15-
},
16+
wallet: vi.fn().mockReturnValue({ l402 }),
17+
_l402: l402,
1618
} as unknown as LnBot & {
17-
l402: {
19+
wallet: ReturnType<typeof vi.fn>;
20+
_l402: {
21+
createChallenge: ReturnType<typeof vi.fn>;
22+
verify: ReturnType<typeof vi.fn>;
1823
pay: ReturnType<typeof vi.fn>;
1924
};
2025
};
@@ -52,12 +57,12 @@ describe("L402 client", () => {
5257
jsonResponse(200, { data: "free" }),
5358
);
5459

55-
const c = client(ln);
60+
const c = client(ln, { walletId: "wal_test" });
5661
const res = await c.fetch(URL_PREMIUM);
5762

5863
expect(res.status).toBe(200);
5964
expect(await res.json()).toEqual({ data: "free" });
60-
expect(ln.l402.pay).not.toHaveBeenCalled();
65+
expect(ln._l402.pay).not.toHaveBeenCalled();
6166
});
6267

6368
it("pays 402 challenge and retries with authorization", async () => {
@@ -74,7 +79,7 @@ describe("L402 client", () => {
7479

7580
globalThis.fetch = mockFetch;
7681

77-
ln.l402.pay.mockResolvedValue({
82+
ln._l402.pay.mockResolvedValue({
7883
authorization: "L402 mac123:preimage_hex",
7984
paymentHash: "hash123",
8085
preimage: "preimage_hex",
@@ -84,12 +89,12 @@ describe("L402 client", () => {
8489
status: "settled",
8590
});
8691

87-
const c = client(ln);
92+
const c = client(ln, { walletId: "wal_test" });
8893
const res = await c.fetch(URL_PREMIUM);
8994

9095
expect(res.status).toBe(200);
9196
expect(await res.json()).toEqual({ data: "premium" });
92-
expect(ln.l402.pay).toHaveBeenCalledWith({ wwwAuthenticate: wwwAuth });
97+
expect(ln._l402.pay).toHaveBeenCalledWith({ wwwAuthenticate: wwwAuth });
9398

9499
// Verify retry had Authorization header
95100
const retryCall = mockFetch.mock.calls[1];
@@ -111,7 +116,7 @@ describe("L402 client", () => {
111116

112117
globalThis.fetch = mockFetch;
113118

114-
ln.l402.pay.mockResolvedValue({
119+
ln._l402.pay.mockResolvedValue({
115120
authorization: "L402 mac:pre",
116121
paymentHash: "hash",
117122
preimage: "pre",
@@ -121,15 +126,15 @@ describe("L402 client", () => {
121126
status: "settled",
122127
});
123128

124-
const c = client(ln);
129+
const c = client(ln, { walletId: "wal_test" });
125130

126131
// First fetch — pays
127132
await c.fetch(URL_PREMIUM);
128-
expect(ln.l402.pay).toHaveBeenCalledTimes(1);
133+
expect(ln._l402.pay).toHaveBeenCalledTimes(1);
129134

130135
// Second fetch — uses cache, no new payment
131136
const res2 = await c.fetch(URL_PREMIUM);
132-
expect(ln.l402.pay).toHaveBeenCalledTimes(1); // Still 1
137+
expect(ln._l402.pay).toHaveBeenCalledTimes(1); // Still 1
133138
expect(res2.status).toBe(200);
134139

135140
// Cached token was sent
@@ -160,7 +165,7 @@ describe("L402 client", () => {
160165

161166
globalThis.fetch = mockFetch;
162167

163-
ln.l402.pay.mockResolvedValue({
168+
ln._l402.pay.mockResolvedValue({
164169
authorization: "L402 mac:pre",
165170
paymentHash: "hash",
166171
preimage: "pre",
@@ -170,15 +175,15 @@ describe("L402 client", () => {
170175
status: "settled",
171176
});
172177

173-
const c = client(ln);
178+
const c = client(ln, { walletId: "wal_test" });
174179

175180
// First fetch — pays
176181
await c.fetch(URL_PREMIUM);
177-
expect(ln.l402.pay).toHaveBeenCalledTimes(1);
182+
expect(ln._l402.pay).toHaveBeenCalledTimes(1);
178183

179184
// Second fetch — cached token rejected, re-pays
180185
const res2 = await c.fetch(URL_PREMIUM);
181-
expect(ln.l402.pay).toHaveBeenCalledTimes(2);
186+
expect(ln._l402.pay).toHaveBeenCalledTimes(2);
182187
expect(res2.status).toBe(200);
183188
});
184189

@@ -190,11 +195,11 @@ describe("L402 client", () => {
190195
),
191196
);
192197

193-
const c = client(ln, { maxPrice: 100 });
198+
const c = client(ln, { walletId: "wal_test", maxPrice: 100 });
194199

195200
await expect(c.fetch(URL_PREMIUM)).rejects.toThrow(L402BudgetExceededError);
196201
await expect(c.fetch(URL_PREMIUM)).rejects.toThrow("exceeds maxPrice");
197-
expect(ln.l402.pay).not.toHaveBeenCalled();
202+
expect(ln._l402.pay).not.toHaveBeenCalled();
198203
});
199204

200205
it("throws L402BudgetExceededError when budget is exhausted", async () => {
@@ -211,7 +216,7 @@ describe("L402 client", () => {
211216
return Promise.resolve(jsonResponse(200, { data: "ok" }));
212217
});
213218

214-
ln.l402.pay.mockResolvedValue({
219+
ln._l402.pay.mockResolvedValue({
215220
authorization: "L402 mac:pre",
216221
paymentHash: "hash",
217222
preimage: "pre",
@@ -222,6 +227,7 @@ describe("L402 client", () => {
222227
});
223228

224229
const c = client(ln, {
230+
walletId: "wal_test",
225231
budgetSats: 100,
226232
budgetPeriod: "day",
227233
store: "none", // Disable cache so each request pays
@@ -239,7 +245,7 @@ describe("L402 client", () => {
239245
jsonResponse(402, { error: "pay up" }),
240246
);
241247

242-
const c = client(ln);
248+
const c = client(ln, { walletId: "wal_test" });
243249

244250
await expect(c.fetch(URL_PREMIUM)).rejects.toThrow(
245251
"402 response missing WWW-Authenticate header",
@@ -252,7 +258,7 @@ describe("L402 client", () => {
252258
jsonResponse(402, { price: 10 }, { "www-authenticate": wwwAuth }),
253259
);
254260

255-
ln.l402.pay.mockResolvedValue({
261+
ln._l402.pay.mockResolvedValue({
256262
authorization: null,
257263
paymentHash: "hash",
258264
preimage: null,
@@ -262,7 +268,7 @@ describe("L402 client", () => {
262268
status: "failed",
263269
});
264270

265-
const c = client(ln);
271+
const c = client(ln, { walletId: "wal_test" });
266272

267273
await expect(c.fetch(URL_PREMIUM)).rejects.toThrow(L402PaymentFailedError);
268274
});
@@ -272,7 +278,7 @@ describe("L402 client", () => {
272278
jsonResponse(200, { result: 42 }),
273279
);
274280

275-
const c = client(ln);
281+
const c = client(ln, { walletId: "wal_test" });
276282
const data = await c.get(URL_PREMIUM);
277283

278284
expect(data).toEqual({ result: 42 });
@@ -283,7 +289,7 @@ describe("L402 client", () => {
283289
jsonResponse(200, { created: true }),
284290
);
285291

286-
const c = client(ln);
292+
const c = client(ln, { walletId: "wal_test" });
287293
const data = await c.post(URL_PREMIUM, {
288294
body: JSON.stringify({ query: "test" }),
289295
});
@@ -298,7 +304,7 @@ describe("L402 client", () => {
298304
jsonResponse(200, { updated: true }),
299305
);
300306

301-
const c = client(ln);
307+
const c = client(ln, { walletId: "wal_test" });
302308
const data = await c.put(URL_PREMIUM, {
303309
body: JSON.stringify({ name: "new" }),
304310
});
@@ -313,7 +319,7 @@ describe("L402 client", () => {
313319
jsonResponse(200, { patched: true }),
314320
);
315321

316-
const c = client(ln);
322+
const c = client(ln, { walletId: "wal_test" });
317323
const data = await c.patch(URL_PREMIUM, {
318324
body: JSON.stringify({ field: "value" }),
319325
});
@@ -328,7 +334,7 @@ describe("L402 client", () => {
328334
jsonResponse(200, { deleted: true }),
329335
);
330336

331-
const c = client(ln);
337+
const c = client(ln, { walletId: "wal_test" });
332338
const data = await c.delete(URL_PREMIUM);
333339

334340
expect(data).toEqual({ deleted: true });
@@ -346,7 +352,7 @@ describe("L402 client", () => {
346352
jsonResponse(402, { price: 10 }, { "www-authenticate": wwwAuth }),
347353
);
348354

349-
ln.l402.pay.mockResolvedValue({
355+
ln._l402.pay.mockResolvedValue({
350356
authorization: "L402 mac:pre",
351357
paymentHash: "hash",
352358
preimage: "pre",
@@ -356,7 +362,7 @@ describe("L402 client", () => {
356362
status: "settled",
357363
});
358364

359-
const c = client(ln);
365+
const c = client(ln, { walletId: "wal_test" });
360366

361367
await expect(c.fetch(URL_PREMIUM)).rejects.toSatisfy((err: unknown) => {
362368
expect(err).toBeInstanceOf(L402PaymentFailedError);
@@ -365,7 +371,7 @@ describe("L402 client", () => {
365371
);
366372
return true;
367373
});
368-
expect(ln.l402.pay).toHaveBeenCalledTimes(1);
374+
expect(ln._l402.pay).toHaveBeenCalledTimes(1);
369375
});
370376

371377
it("defaults maxPrice to 1000 sats", async () => {
@@ -376,7 +382,7 @@ describe("L402 client", () => {
376382
),
377383
);
378384

379-
const c = client(ln); // No maxPrice specified
385+
const c = client(ln, { walletId: "wal_test" }); // No maxPrice specified
380386

381387
await expect(c.fetch(URL_PREMIUM)).rejects.toThrow("exceeds maxPrice 1000");
382388
});

0 commit comments

Comments
 (0)