@lnbot/l402 0.1.0

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 ADDED
@@ -0,0 +1,309 @@
1
+ # @lnbot/l402
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@lnbot/l402)](https://www.npmjs.com/package/@lnbot/l402)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@lnbot/l402)](https://www.npmjs.com/package/@lnbot/l402)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@lnbot/l402)](https://bundlephobia.com/package/@lnbot/l402)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
8
+
9
+ **L402 payment middleware for Express.js** — paywall any API in one line. Built on [ln.bot](https://ln.bot).
10
+
11
+ Add Lightning-powered pay-per-request to any Express API. Protect premium routes with a paywall, or build clients that auto-pay L402-protected services — all without touching any cryptography.
12
+
13
+ ```typescript
14
+ import express from "express";
15
+ import { l402, LnBot } from "@lnbot/l402";
16
+
17
+ const app = express();
18
+ const ln = new LnBot({ apiKey: "key_..." });
19
+
20
+ app.use("/api/premium", l402.paywall(ln, { price: 10 }));
21
+
22
+ app.get("/api/premium/data", (req, res) => {
23
+ res.json({ data: "premium content" });
24
+ });
25
+ ```
26
+
27
+ > This package is a thin glue layer. All L402 logic — macaroon creation, signature verification, preimage checking — lives in the [ln.bot API](https://ln.bot/docs) via [`@lnbot/sdk`](https://www.npmjs.com/package/@lnbot/sdk). Zero crypto dependencies.
28
+
29
+ ---
30
+
31
+ ## What is L402?
32
+
33
+ [L402](https://github.com/lightninglabs/L402) is a protocol built on HTTP `402 Payment Required`. It enables machine-to-machine micropayments over the Lightning Network:
34
+
35
+ 1. **Client** requests a protected resource
36
+ 2. **Server** returns `402` with a Lightning invoice and a macaroon token
37
+ 3. **Client** pays the invoice, obtains the preimage as proof of payment
38
+ 4. **Client** retries the request with `Authorization: L402 <macaroon>:<preimage>`
39
+ 5. **Server** verifies the token and grants access
40
+
41
+ L402 is ideal for API monetization, AI agent tool access, pay-per-request data feeds, and any scenario where you want instant, permissionless, per-request payments without subscriptions or API key provisioning.
42
+
43
+ ---
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ npm install @lnbot/l402
49
+ ```
50
+
51
+ ```bash
52
+ pnpm add @lnbot/l402
53
+ ```
54
+
55
+ ```bash
56
+ yarn add @lnbot/l402
57
+ ```
58
+
59
+ `@lnbot/sdk` and `express` are peer dependencies and will be resolved automatically.
60
+
61
+ ---
62
+
63
+ ## Server — Protect Routes with L402
64
+
65
+ The `l402.paywall()` middleware intercepts requests, verifies L402 tokens via the SDK, and issues new challenges when payment is needed. Two SDK calls, ~40 lines of glue code, zero crypto.
66
+
67
+ ```typescript
68
+ import express from "express";
69
+ import { l402, LnBot } from "@lnbot/l402";
70
+
71
+ const app = express();
72
+ const ln = new LnBot({ apiKey: "key_..." });
73
+
74
+ // Paywall a route group — 10 sats per request
75
+ app.use("/api/premium", l402.paywall(ln, {
76
+ price: 10,
77
+ description: "API access",
78
+ }));
79
+
80
+ app.get("/api/premium/data", (req, res) => {
81
+ // req.l402 is populated after successful payment verification
82
+ res.json({
83
+ data: "premium content",
84
+ paymentHash: req.l402?.paymentHash,
85
+ });
86
+ });
87
+
88
+ // Free routes still work normally
89
+ app.get("/api/free/health", (req, res) => {
90
+ res.json({ status: "ok" });
91
+ });
92
+
93
+ app.listen(3000);
94
+ ```
95
+
96
+ ### How the middleware works
97
+
98
+ 1. Checks for an `Authorization: L402 ...` header
99
+ 2. If present, calls `ln.l402.verify()` — the SDK checks signature, preimage, and caveats server-side
100
+ 3. If valid, populates `req.l402` and calls `next()`
101
+ 4. If missing or invalid, calls `ln.l402.createChallenge()` and returns a `402` response with the invoice and macaroon
102
+
103
+ ### Dynamic pricing
104
+
105
+ ```typescript
106
+ // Fixed price per route
107
+ app.use("/api/cheap", l402.paywall(ln, { price: 1 }));
108
+ app.use("/api/expensive", l402.paywall(ln, { price: 100 }));
109
+
110
+ // Custom pricing function — receives the request, returns price in sats
111
+ app.use("/api/dynamic", l402.paywall(ln, {
112
+ price: (req) => {
113
+ if (req.path.includes("/bulk")) return 50;
114
+ return 5;
115
+ },
116
+ }));
117
+ ```
118
+
119
+ ### Paywall options
120
+
121
+ | Option | Type | Description |
122
+ | --- | --- | --- |
123
+ | `price` | `number \| (req) => number` | Price in satoshis — fixed or per-request |
124
+ | `description` | `string` | Invoice memo shown in wallets |
125
+ | `expirySeconds` | `number` | Challenge expiry in seconds |
126
+ | `caveats` | `string[]` | Macaroon caveats to attach |
127
+
128
+ ---
129
+
130
+ ## Client — Auto-Pay L402 APIs
131
+
132
+ The `l402.client()` wrapper makes L402 payment transparent. It detects `402` responses, pays the Lightning invoice via the SDK, caches the token, and retries — all in one `fetch` call.
133
+
134
+ ```typescript
135
+ import { l402, LnBot } from "@lnbot/l402";
136
+
137
+ const ln = new LnBot({ apiKey: "key_..." });
138
+
139
+ const client = l402.client(ln, {
140
+ maxPrice: 100, // refuse to pay more than 100 sats per request
141
+ budgetSats: 50000, // spending limit for the period
142
+ budgetPeriod: "day", // reset period: "hour" | "day" | "week" | "month"
143
+ store: "memory", // token cache: "memory" (default) | "none" | custom TokenStore
144
+ });
145
+
146
+ // Use like fetch — L402 payment is transparent
147
+ const response = await client.fetch("https://api.example.com/premium/data");
148
+ const data = await response.json();
149
+
150
+ // Convenience methods
151
+ const json = await client.get("https://api.example.com/premium/data");
152
+ const result = await client.post("https://api.example.com/premium/submit", {
153
+ body: JSON.stringify({ query: "test" }),
154
+ });
155
+ ```
156
+
157
+ ### How the client works
158
+
159
+ 1. Checks the token cache for a valid credential
160
+ 2. If cached, sends the request with the `Authorization` header
161
+ 3. If no cache (or server rejects), makes a plain request
162
+ 4. On `402`, parses the challenge and checks budget limits
163
+ 5. Calls `ln.l402.pay()` — the SDK pays the invoice and returns a ready-to-use token
164
+ 6. Caches the token and retries the request with authorization
165
+
166
+ ### Client options
167
+
168
+ | Option | Type | Default | Description |
169
+ | --- | --- | --- | --- |
170
+ | `maxPrice` | `number` | `1000` | Max sats to pay for a single request |
171
+ | `budgetSats` | `number` | unlimited | Total budget in sats for the period |
172
+ | `budgetPeriod` | `string` | — | Reset period: `"hour"`, `"day"`, `"week"`, `"month"` |
173
+ | `store` | `string \| TokenStore` | `"memory"` | Token cache: `"memory"`, `"none"`, or custom |
174
+
175
+ ### Custom token store
176
+
177
+ Implement the `TokenStore` interface for Redis, file system, or any persistence layer:
178
+
179
+ ```typescript
180
+ import { l402, LnBot } from "@lnbot/l402";
181
+ import type { TokenStore } from "@lnbot/l402";
182
+
183
+ const ln = new LnBot({ apiKey: "key_..." });
184
+
185
+ const redisStore: TokenStore = {
186
+ async get(url) { /* read from Redis */ },
187
+ async set(url, token) { /* write to Redis */ },
188
+ async delete(url) { /* delete from Redis */ },
189
+ };
190
+
191
+ const client = l402.client(ln, { store: redisStore });
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Header Utilities
197
+
198
+ Parse and format L402 headers for custom integrations:
199
+
200
+ ```typescript
201
+ import { l402 } from "@lnbot/l402";
202
+
203
+ // Parse Authorization: L402 <macaroon>:<preimage>
204
+ l402.parseAuthorization("L402 mac_base64:preimage_hex");
205
+ // → { macaroon: "mac_base64", preimage: "preimage_hex" }
206
+
207
+ // Parse WWW-Authenticate: L402 macaroon="...", invoice="..."
208
+ l402.parseChallenge('L402 macaroon="abc", invoice="lnbc1..."');
209
+ // → { macaroon: "abc", invoice: "lnbc1..." }
210
+
211
+ // Format headers
212
+ l402.formatAuthorization("mac_base64", "preimage_hex");
213
+ // → "L402 mac_base64:preimage_hex"
214
+
215
+ l402.formatChallenge("abc", "lnbc1...");
216
+ // → 'L402 macaroon="abc", invoice="lnbc1..."'
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Error Handling
222
+
223
+ ```typescript
224
+ import { L402Error, L402BudgetExceededError, L402PaymentFailedError } from "@lnbot/l402";
225
+
226
+ try {
227
+ const data = await client.get("https://api.example.com/expensive");
228
+ } catch (err) {
229
+ if (err instanceof L402BudgetExceededError) {
230
+ // Price exceeds maxPrice or total budget exhausted
231
+ } else if (err instanceof L402PaymentFailedError) {
232
+ // Lightning payment failed or didn't settle
233
+ } else if (err instanceof L402Error) {
234
+ // Other L402 protocol error (missing header, parse failure)
235
+ }
236
+ }
237
+ ```
238
+
239
+ ---
240
+
241
+ ## API Reference
242
+
243
+ ### Server
244
+
245
+ | Export | Description |
246
+ | --- | --- |
247
+ | `l402.paywall(ln, options)` | Express middleware factory — protects routes behind an L402 paywall |
248
+
249
+ ### Client
250
+
251
+ | Export | Description |
252
+ | --- | --- |
253
+ | `l402.client(ln, options?)` | Creates an L402-aware HTTP client with automatic payment |
254
+
255
+ ### Header Utilities
256
+
257
+ | Export | Description |
258
+ | --- | --- |
259
+ | `l402.parseAuthorization(header)` | Parse `Authorization: L402 ...` into `{ macaroon, preimage }` |
260
+ | `l402.parseChallenge(header)` | Parse `WWW-Authenticate: L402 ...` into `{ macaroon, invoice }` |
261
+ | `l402.formatAuthorization(macaroon, preimage)` | Format an `Authorization` header value |
262
+ | `l402.formatChallenge(macaroon, invoice)` | Format a `WWW-Authenticate` header value |
263
+
264
+ ### Types
265
+
266
+ | Type | Description |
267
+ | --- | --- |
268
+ | `L402PaywallOptions` | Options for `l402.paywall()` |
269
+ | `L402ClientOptions` | Options for `l402.client()` |
270
+ | `L402Token` | Cached L402 credential (macaroon + preimage + metadata) |
271
+ | `TokenStore` | Interface for custom token caches |
272
+ | `L402RequestData` | Data attached to `req.l402` after verification |
273
+
274
+ ### Errors
275
+
276
+ | Class | Description |
277
+ | --- | --- |
278
+ | `L402Error` | Base error for all L402 protocol errors |
279
+ | `L402BudgetExceededError` | Price or cumulative spend exceeds configured limits |
280
+ | `L402PaymentFailedError` | Lightning payment failed or didn't return authorization |
281
+
282
+ ---
283
+
284
+ ## Requirements
285
+
286
+ - **Node.js 18+**, Bun, or Deno
287
+ - **Express 4+** (server middleware)
288
+ - An [ln.bot](https://ln.bot) API key — [create a wallet](https://ln.bot/docs) to get one
289
+
290
+ ---
291
+
292
+ ## Related packages
293
+
294
+ - [`@lnbot/sdk`](https://www.npmjs.com/package/@lnbot/sdk) — The TypeScript SDK this package is built on
295
+ - [Python SDK](https://github.com/lnbotdev/python-sdk) · [pypi](https://pypi.org/project/lnbot/)
296
+ - [Go SDK](https://github.com/lnbotdev/go-sdk) · [pkg.go.dev](https://pkg.go.dev/github.com/lnbotdev/go-sdk)
297
+ - [Rust SDK](https://github.com/lnbotdev/rust-sdk) · [crates.io](https://crates.io/crates/lnbot)
298
+
299
+ ## Links
300
+
301
+ - [ln.bot](https://ln.bot) — website
302
+ - [Documentation](https://ln.bot/docs)
303
+ - [L402 specification](https://github.com/lightninglabs/L402)
304
+ - [GitHub](https://github.com/lnbotdev)
305
+ - [npm](https://www.npmjs.com/package/@lnbot/l402)
306
+
307
+ ## License
308
+
309
+ MIT
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client/index.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ Budget: () => Budget,
24
+ MemoryStore: () => MemoryStore,
25
+ NoStore: () => NoStore,
26
+ client: () => client,
27
+ resolveStore: () => resolveStore
28
+ });
29
+ module.exports = __toCommonJS(client_exports);
30
+
31
+ // src/errors.ts
32
+ var L402Error = class extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "L402Error";
36
+ }
37
+ };
38
+ var L402BudgetExceededError = class extends L402Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "L402BudgetExceededError";
42
+ }
43
+ };
44
+ var L402PaymentFailedError = class extends L402Error {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = "L402PaymentFailedError";
48
+ }
49
+ };
50
+
51
+ // src/server/headers.ts
52
+ function parseAuthorization(header) {
53
+ if (!header.startsWith("L402 ")) return null;
54
+ const token = header.slice(5);
55
+ const colonIndex = token.lastIndexOf(":");
56
+ if (colonIndex === -1) return null;
57
+ return {
58
+ macaroon: token.slice(0, colonIndex),
59
+ preimage: token.slice(colonIndex + 1)
60
+ };
61
+ }
62
+ function parseChallenge(header) {
63
+ if (!header.startsWith("L402 ")) return null;
64
+ const macaroonMatch = header.match(/macaroon="([^"]+)"/);
65
+ const invoiceMatch = header.match(/invoice="([^"]+)"/);
66
+ if (!macaroonMatch || !invoiceMatch) return null;
67
+ return { macaroon: macaroonMatch[1], invoice: invoiceMatch[1] };
68
+ }
69
+
70
+ // src/client/budget.ts
71
+ var PERIOD_MS = {
72
+ hour: 60 * 60 * 1e3,
73
+ day: 24 * 60 * 60 * 1e3,
74
+ week: 7 * 24 * 60 * 60 * 1e3,
75
+ month: 30 * 24 * 60 * 60 * 1e3
76
+ };
77
+ var Budget = class {
78
+ spent = 0;
79
+ periodStart = Date.now();
80
+ totalSats;
81
+ periodMs;
82
+ constructor(options) {
83
+ this.totalSats = options.budgetSats;
84
+ if (options.budgetPeriod) {
85
+ this.periodMs = PERIOD_MS[options.budgetPeriod];
86
+ }
87
+ }
88
+ maybeReset() {
89
+ if (this.periodMs && Date.now() - this.periodStart >= this.periodMs) {
90
+ this.spent = 0;
91
+ this.periodStart = Date.now();
92
+ }
93
+ }
94
+ /** Throws L402BudgetExceededError if spending `price` would exceed the budget. */
95
+ check(price) {
96
+ if (this.totalSats === void 0) return;
97
+ this.maybeReset();
98
+ if (this.spent + price > this.totalSats) {
99
+ throw new L402BudgetExceededError(
100
+ `Payment of ${price} sats would exceed budget (${this.spent}/${this.totalSats} sats spent)`
101
+ );
102
+ }
103
+ }
104
+ /** Record a successful payment. */
105
+ record(price) {
106
+ this.maybeReset();
107
+ this.spent += price;
108
+ }
109
+ };
110
+
111
+ // src/client/store.ts
112
+ function normalizeUrl(url) {
113
+ try {
114
+ const u = new URL(url);
115
+ u.search = "";
116
+ u.hash = "";
117
+ return u.toString().replace(/\/+$/, "");
118
+ } catch {
119
+ return url;
120
+ }
121
+ }
122
+ var MemoryStore = class {
123
+ tokens = /* @__PURE__ */ new Map();
124
+ async get(url) {
125
+ return this.tokens.get(normalizeUrl(url)) ?? null;
126
+ }
127
+ async set(url, token) {
128
+ this.tokens.set(normalizeUrl(url), token);
129
+ }
130
+ async delete(url) {
131
+ this.tokens.delete(normalizeUrl(url));
132
+ }
133
+ };
134
+ var NoStore = class {
135
+ async get() {
136
+ return null;
137
+ }
138
+ async set() {
139
+ }
140
+ async delete() {
141
+ }
142
+ };
143
+ function resolveStore(store) {
144
+ if (!store || store === "memory") return new MemoryStore();
145
+ if (store === "none") return new NoStore();
146
+ return store;
147
+ }
148
+
149
+ // src/client/fetch.ts
150
+ function client(ln, options = {}) {
151
+ const store = resolveStore(options.store);
152
+ const budget = new Budget(options);
153
+ const maxPrice = options.maxPrice ?? 1e3;
154
+ async function l402Fetch(url, init) {
155
+ const cached = await store.get(url);
156
+ if (cached) {
157
+ const isExpired = cached.expiresAt && cached.expiresAt.getTime() <= Date.now();
158
+ if (!isExpired) {
159
+ const headers = new Headers(init?.headers);
160
+ headers.set("Authorization", cached.authorization);
161
+ const res2 = await globalThis.fetch(url, { ...init, headers });
162
+ if (res2.status !== 402) return res2;
163
+ await store.delete(url);
164
+ } else {
165
+ await store.delete(url);
166
+ }
167
+ }
168
+ const res = await globalThis.fetch(url, init);
169
+ if (res.status !== 402) return res;
170
+ const wwwAuth = res.headers.get("www-authenticate");
171
+ if (!wwwAuth)
172
+ throw new L402Error("402 response missing WWW-Authenticate header");
173
+ const challenge = parseChallenge(wwwAuth);
174
+ if (!challenge) throw new L402Error("Could not parse L402 challenge");
175
+ const body = await res.json().catch(() => null);
176
+ const price = body?.price ?? 0;
177
+ if (price > maxPrice) {
178
+ throw new L402Error(
179
+ `Price ${price} sats exceeds maxPrice ${maxPrice}`
180
+ );
181
+ }
182
+ budget.check(price);
183
+ const payment = await ln.l402.pay({ wwwAuthenticate: wwwAuth });
184
+ if (payment.status === "failed") {
185
+ throw new L402PaymentFailedError("L402 payment failed");
186
+ }
187
+ if (!payment.authorization) {
188
+ throw new L402PaymentFailedError(
189
+ "Payment did not return authorization token"
190
+ );
191
+ }
192
+ const parsed = parseAuthorization(payment.authorization);
193
+ await store.set(url, {
194
+ macaroon: parsed?.macaroon ?? challenge.macaroon,
195
+ preimage: payment.preimage ?? parsed?.preimage ?? "",
196
+ authorization: payment.authorization,
197
+ paidAt: /* @__PURE__ */ new Date(),
198
+ expiresAt: body?.expiresAt ? new Date(body.expiresAt) : void 0
199
+ });
200
+ budget.record(price);
201
+ const retryHeaders = new Headers(init?.headers);
202
+ retryHeaders.set("Authorization", payment.authorization);
203
+ return globalThis.fetch(url, { ...init, headers: retryHeaders });
204
+ }
205
+ return {
206
+ fetch: l402Fetch,
207
+ async get(url, init) {
208
+ const res = await l402Fetch(url, { ...init, method: "GET" });
209
+ return res.json();
210
+ },
211
+ async post(url, init) {
212
+ const res = await l402Fetch(url, { ...init, method: "POST" });
213
+ return res.json();
214
+ }
215
+ };
216
+ }
217
+ // Annotate the CommonJS export names for ESM import in node:
218
+ 0 && (module.exports = {
219
+ Budget,
220
+ MemoryStore,
221
+ NoStore,
222
+ client,
223
+ resolveStore
224
+ });
225
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/index.ts","../../src/errors.ts","../../src/server/headers.ts","../../src/client/budget.ts","../../src/client/store.ts","../../src/client/fetch.ts"],"sourcesContent":["export { client, type L402Client } from \"./fetch.js\";\nexport { Budget } from \"./budget.js\";\nexport { MemoryStore, NoStore, resolveStore } from \"./store.js\";\n","export class L402Error extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"L402Error\";\n }\n}\n\nexport class L402BudgetExceededError extends L402Error {\n constructor(message: string) {\n super(message);\n this.name = \"L402BudgetExceededError\";\n }\n}\n\nexport class L402PaymentFailedError extends L402Error {\n constructor(message: string) {\n super(message);\n this.name = \"L402PaymentFailedError\";\n }\n}\n","/** Parse an L402 Authorization header into { macaroon, preimage }. */\nexport function parseAuthorization(\n header: string,\n): { macaroon: string; preimage: string } | null {\n if (!header.startsWith(\"L402 \")) return null;\n const token = header.slice(5);\n const colonIndex = token.lastIndexOf(\":\");\n if (colonIndex === -1) return null;\n return {\n macaroon: token.slice(0, colonIndex),\n preimage: token.slice(colonIndex + 1),\n };\n}\n\n/** Parse a WWW-Authenticate: L402 header into { macaroon, invoice }. */\nexport function parseChallenge(\n header: string,\n): { macaroon: string; invoice: string } | null {\n if (!header.startsWith(\"L402 \")) return null;\n const macaroonMatch = header.match(/macaroon=\"([^\"]+)\"/);\n const invoiceMatch = header.match(/invoice=\"([^\"]+)\"/);\n if (!macaroonMatch || !invoiceMatch) return null;\n return { macaroon: macaroonMatch[1], invoice: invoiceMatch[1] };\n}\n\n/** Format an Authorization header value. */\nexport function formatAuthorization(\n macaroon: string,\n preimage: string,\n): string {\n return `L402 ${macaroon}:${preimage}`;\n}\n\n/** Format a WWW-Authenticate header value. */\nexport function formatChallenge(\n macaroon: string,\n invoice: string,\n): string {\n return `L402 macaroon=\"${macaroon}\", invoice=\"${invoice}\"`;\n}\n","import type { L402ClientOptions } from \"../types.js\";\nimport { L402BudgetExceededError } from \"../errors.js\";\n\nconst PERIOD_MS: Record<string, number> = {\n hour: 60 * 60 * 1000,\n day: 24 * 60 * 60 * 1000,\n week: 7 * 24 * 60 * 60 * 1000,\n month: 30 * 24 * 60 * 60 * 1000,\n};\n\n/** In-memory budget tracker with periodic resets. */\nexport class Budget {\n private spent = 0;\n private periodStart = Date.now();\n private readonly totalSats: number | undefined;\n private readonly periodMs: number | undefined;\n\n constructor(options: L402ClientOptions) {\n this.totalSats = options.budgetSats;\n if (options.budgetPeriod) {\n this.periodMs = PERIOD_MS[options.budgetPeriod];\n }\n }\n\n private maybeReset(): void {\n if (this.periodMs && Date.now() - this.periodStart >= this.periodMs) {\n this.spent = 0;\n this.periodStart = Date.now();\n }\n }\n\n /** Throws L402BudgetExceededError if spending `price` would exceed the budget. */\n check(price: number): void {\n if (this.totalSats === undefined) return;\n this.maybeReset();\n if (this.spent + price > this.totalSats) {\n throw new L402BudgetExceededError(\n `Payment of ${price} sats would exceed budget (${this.spent}/${this.totalSats} sats spent)`,\n );\n }\n }\n\n /** Record a successful payment. */\n record(price: number): void {\n this.maybeReset();\n this.spent += price;\n }\n}\n","import type { TokenStore, L402Token } from \"../types.js\";\n\n/** Strip query params, hash, and trailing slashes for consistent cache keys. */\nfunction normalizeUrl(url: string): string {\n try {\n const u = new URL(url);\n u.search = \"\";\n u.hash = \"\";\n return u.toString().replace(/\\/+$/, \"\");\n } catch {\n return url;\n }\n}\n\n/** Default in-memory token cache backed by a Map. */\nexport class MemoryStore implements TokenStore {\n private tokens = new Map<string, L402Token>();\n\n async get(url: string): Promise<L402Token | null> {\n return this.tokens.get(normalizeUrl(url)) ?? null;\n }\n\n async set(url: string, token: L402Token): Promise<void> {\n this.tokens.set(normalizeUrl(url), token);\n }\n\n async delete(url: string): Promise<void> {\n this.tokens.delete(normalizeUrl(url));\n }\n}\n\n/** No-op store — never caches, every request pays fresh. */\nexport class NoStore implements TokenStore {\n async get(): Promise<null> {\n return null;\n }\n async set(): Promise<void> {}\n async delete(): Promise<void> {}\n}\n\n/** Resolve the `store` option into a concrete TokenStore. */\nexport function resolveStore(\n store?: \"memory\" | \"none\" | TokenStore,\n): TokenStore {\n if (!store || store === \"memory\") return new MemoryStore();\n if (store === \"none\") return new NoStore();\n return store;\n}\n","import type { LnBot } from \"@lnbot/sdk\";\nimport type { L402ClientOptions } from \"../types.js\";\nimport {\n L402Error,\n L402PaymentFailedError,\n} from \"../errors.js\";\nimport { parseChallenge, parseAuthorization } from \"../server/headers.js\";\nimport { Budget } from \"./budget.js\";\nimport { resolveStore } from \"./store.js\";\n\n/** An L402-aware HTTP client that transparently pays Lightning invoices on 402 responses. */\nexport interface L402Client {\n /** L402-aware fetch — pays 402 challenges automatically. */\n fetch(url: string, init?: RequestInit): Promise<Response>;\n /** GET + JSON parse with automatic L402 payment. */\n get(url: string, init?: RequestInit): Promise<unknown>;\n /** POST + JSON parse with automatic L402 payment. */\n post(url: string, init?: RequestInit): Promise<unknown>;\n}\n\n/**\n * Create an L402-aware HTTP client.\n *\n * One SDK call: `ln.l402.pay()` — pays the invoice and returns the Authorization token.\n */\nexport function client(ln: LnBot, options: L402ClientOptions = {}): L402Client {\n const store = resolveStore(options.store);\n const budget = new Budget(options);\n const maxPrice = options.maxPrice ?? 1000;\n\n async function l402Fetch(\n url: string,\n init?: RequestInit,\n ): Promise<Response> {\n // Step 1: Check token cache\n const cached = await store.get(url);\n if (cached) {\n const isExpired =\n cached.expiresAt && cached.expiresAt.getTime() <= Date.now();\n if (!isExpired) {\n const headers = new Headers(init?.headers);\n headers.set(\"Authorization\", cached.authorization);\n const res = await globalThis.fetch(url, { ...init, headers });\n // If the cached token was rejected (expired server-side), fall through\n if (res.status !== 402) return res;\n await store.delete(url);\n } else {\n await store.delete(url);\n }\n }\n\n // Step 2: Make request without auth\n const res = await globalThis.fetch(url, init);\n if (res.status !== 402) return res;\n\n // Step 3: Parse the 402 challenge\n const wwwAuth = res.headers.get(\"www-authenticate\");\n if (!wwwAuth)\n throw new L402Error(\"402 response missing WWW-Authenticate header\");\n\n const challenge = parseChallenge(wwwAuth);\n if (!challenge) throw new L402Error(\"Could not parse L402 challenge\");\n\n // Parse body for price info\n const body = await res.json().catch(() => null);\n const price: number = body?.price ?? 0;\n\n // Step 4: Budget checks\n if (price > maxPrice) {\n throw new L402Error(\n `Price ${price} sats exceeds maxPrice ${maxPrice}`,\n );\n }\n budget.check(price);\n\n // Step 5: Pay via SDK\n const payment = await ln.l402.pay({ wwwAuthenticate: wwwAuth });\n\n if (payment.status === \"failed\") {\n throw new L402PaymentFailedError(\"L402 payment failed\");\n }\n if (!payment.authorization) {\n throw new L402PaymentFailedError(\n \"Payment did not return authorization token\",\n );\n }\n\n // Step 6: Cache the token\n const parsed = parseAuthorization(payment.authorization);\n await store.set(url, {\n macaroon: parsed?.macaroon ?? challenge.macaroon,\n preimage: payment.preimage ?? parsed?.preimage ?? \"\",\n authorization: payment.authorization,\n paidAt: new Date(),\n expiresAt: body?.expiresAt\n ? new Date(body.expiresAt)\n : undefined,\n });\n\n budget.record(price);\n\n // Step 7: Retry with L402 Authorization\n const retryHeaders = new Headers(init?.headers);\n retryHeaders.set(\"Authorization\", payment.authorization);\n return globalThis.fetch(url, { ...init, headers: retryHeaders });\n }\n\n return {\n fetch: l402Fetch,\n async get(url: string, init?: RequestInit) {\n const res = await l402Fetch(url, { ...init, method: \"GET\" });\n return res.json();\n },\n async post(url: string, init?: RequestInit) {\n const res = await l402Fetch(url, { ...init, method: \"POST\" });\n return res.json();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,0BAAN,cAAsC,UAAU;AAAA,EACrD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,yBAAN,cAAqC,UAAU;AAAA,EACpD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;;AClBO,SAAS,mBACd,QAC+C;AAC/C,MAAI,CAAC,OAAO,WAAW,OAAO,EAAG,QAAO;AACxC,QAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,QAAM,aAAa,MAAM,YAAY,GAAG;AACxC,MAAI,eAAe,GAAI,QAAO;AAC9B,SAAO;AAAA,IACL,UAAU,MAAM,MAAM,GAAG,UAAU;AAAA,IACnC,UAAU,MAAM,MAAM,aAAa,CAAC;AAAA,EACtC;AACF;AAGO,SAAS,eACd,QAC8C;AAC9C,MAAI,CAAC,OAAO,WAAW,OAAO,EAAG,QAAO;AACxC,QAAM,gBAAgB,OAAO,MAAM,oBAAoB;AACvD,QAAM,eAAe,OAAO,MAAM,mBAAmB;AACrD,MAAI,CAAC,iBAAiB,CAAC,aAAc,QAAO;AAC5C,SAAO,EAAE,UAAU,cAAc,CAAC,GAAG,SAAS,aAAa,CAAC,EAAE;AAChE;;;ACpBA,IAAM,YAAoC;AAAA,EACxC,MAAM,KAAK,KAAK;AAAA,EAChB,KAAK,KAAK,KAAK,KAAK;AAAA,EACpB,MAAM,IAAI,KAAK,KAAK,KAAK;AAAA,EACzB,OAAO,KAAK,KAAK,KAAK,KAAK;AAC7B;AAGO,IAAM,SAAN,MAAa;AAAA,EACV,QAAQ;AAAA,EACR,cAAc,KAAK,IAAI;AAAA,EACd;AAAA,EACA;AAAA,EAEjB,YAAY,SAA4B;AACtC,SAAK,YAAY,QAAQ;AACzB,QAAI,QAAQ,cAAc;AACxB,WAAK,WAAW,UAAU,QAAQ,YAAY;AAAA,IAChD;AAAA,EACF;AAAA,EAEQ,aAAmB;AACzB,QAAI,KAAK,YAAY,KAAK,IAAI,IAAI,KAAK,eAAe,KAAK,UAAU;AACnE,WAAK,QAAQ;AACb,WAAK,cAAc,KAAK,IAAI;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,OAAqB;AACzB,QAAI,KAAK,cAAc,OAAW;AAClC,SAAK,WAAW;AAChB,QAAI,KAAK,QAAQ,QAAQ,KAAK,WAAW;AACvC,YAAM,IAAI;AAAA,QACR,cAAc,KAAK,8BAA8B,KAAK,KAAK,IAAI,KAAK,SAAS;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,OAAqB;AAC1B,SAAK,WAAW;AAChB,SAAK,SAAS;AAAA,EAChB;AACF;;;AC5CA,SAAS,aAAa,KAAqB;AACzC,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,MAAE,SAAS;AACX,MAAE,OAAO;AACT,WAAO,EAAE,SAAS,EAAE,QAAQ,QAAQ,EAAE;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,IAAM,cAAN,MAAwC;AAAA,EACrC,SAAS,oBAAI,IAAuB;AAAA,EAE5C,MAAM,IAAI,KAAwC;AAChD,WAAO,KAAK,OAAO,IAAI,aAAa,GAAG,CAAC,KAAK;AAAA,EAC/C;AAAA,EAEA,MAAM,IAAI,KAAa,OAAiC;AACtD,SAAK,OAAO,IAAI,aAAa,GAAG,GAAG,KAAK;AAAA,EAC1C;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,SAAK,OAAO,OAAO,aAAa,GAAG,CAAC;AAAA,EACtC;AACF;AAGO,IAAM,UAAN,MAAoC;AAAA,EACzC,MAAM,MAAqB;AACzB,WAAO;AAAA,EACT;AAAA,EACA,MAAM,MAAqB;AAAA,EAAC;AAAA,EAC5B,MAAM,SAAwB;AAAA,EAAC;AACjC;AAGO,SAAS,aACd,OACY;AACZ,MAAI,CAAC,SAAS,UAAU,SAAU,QAAO,IAAI,YAAY;AACzD,MAAI,UAAU,OAAQ,QAAO,IAAI,QAAQ;AACzC,SAAO;AACT;;;ACtBO,SAAS,OAAO,IAAW,UAA6B,CAAC,GAAe;AAC7E,QAAM,QAAQ,aAAa,QAAQ,KAAK;AACxC,QAAM,SAAS,IAAI,OAAO,OAAO;AACjC,QAAM,WAAW,QAAQ,YAAY;AAErC,iBAAe,UACb,KACA,MACmB;AAEnB,UAAM,SAAS,MAAM,MAAM,IAAI,GAAG;AAClC,QAAI,QAAQ;AACV,YAAM,YACJ,OAAO,aAAa,OAAO,UAAU,QAAQ,KAAK,KAAK,IAAI;AAC7D,UAAI,CAAC,WAAW;AACd,cAAM,UAAU,IAAI,QAAQ,MAAM,OAAO;AACzC,gBAAQ,IAAI,iBAAiB,OAAO,aAAa;AACjD,cAAMA,OAAM,MAAM,WAAW,MAAM,KAAK,EAAE,GAAG,MAAM,QAAQ,CAAC;AAE5D,YAAIA,KAAI,WAAW,IAAK,QAAOA;AAC/B,cAAM,MAAM,OAAO,GAAG;AAAA,MACxB,OAAO;AACL,cAAM,MAAM,OAAO,GAAG;AAAA,MACxB;AAAA,IACF;AAGA,UAAM,MAAM,MAAM,WAAW,MAAM,KAAK,IAAI;AAC5C,QAAI,IAAI,WAAW,IAAK,QAAO;AAG/B,UAAM,UAAU,IAAI,QAAQ,IAAI,kBAAkB;AAClD,QAAI,CAAC;AACH,YAAM,IAAI,UAAU,8CAA8C;AAEpE,UAAM,YAAY,eAAe,OAAO;AACxC,QAAI,CAAC,UAAW,OAAM,IAAI,UAAU,gCAAgC;AAGpE,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAC9C,UAAM,QAAgB,MAAM,SAAS;AAGrC,QAAI,QAAQ,UAAU;AACpB,YAAM,IAAI;AAAA,QACR,SAAS,KAAK,0BAA0B,QAAQ;AAAA,MAClD;AAAA,IACF;AACA,WAAO,MAAM,KAAK;AAGlB,UAAM,UAAU,MAAM,GAAG,KAAK,IAAI,EAAE,iBAAiB,QAAQ,CAAC;AAE9D,QAAI,QAAQ,WAAW,UAAU;AAC/B,YAAM,IAAI,uBAAuB,qBAAqB;AAAA,IACxD;AACA,QAAI,CAAC,QAAQ,eAAe;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,UAAM,SAAS,mBAAmB,QAAQ,aAAa;AACvD,UAAM,MAAM,IAAI,KAAK;AAAA,MACnB,UAAU,QAAQ,YAAY,UAAU;AAAA,MACxC,UAAU,QAAQ,YAAY,QAAQ,YAAY;AAAA,MAClD,eAAe,QAAQ;AAAA,MACvB,QAAQ,oBAAI,KAAK;AAAA,MACjB,WAAW,MAAM,YACb,IAAI,KAAK,KAAK,SAAS,IACvB;AAAA,IACN,CAAC;AAED,WAAO,OAAO,KAAK;AAGnB,UAAM,eAAe,IAAI,QAAQ,MAAM,OAAO;AAC9C,iBAAa,IAAI,iBAAiB,QAAQ,aAAa;AACvD,WAAO,WAAW,MAAM,KAAK,EAAE,GAAG,MAAM,SAAS,aAAa,CAAC;AAAA,EACjE;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM,IAAI,KAAa,MAAoB;AACzC,YAAM,MAAM,MAAM,UAAU,KAAK,EAAE,GAAG,MAAM,QAAQ,MAAM,CAAC;AAC3D,aAAO,IAAI,KAAK;AAAA,IAClB;AAAA,IACA,MAAM,KAAK,KAAa,MAAoB;AAC1C,YAAM,MAAM,MAAM,UAAU,KAAK,EAAE,GAAG,MAAM,QAAQ,OAAO,CAAC;AAC5D,aAAO,IAAI,KAAK;AAAA,IAClB;AAAA,EACF;AACF;","names":["res"]}
@@ -0,0 +1,36 @@
1
+ export { L as L402Client, c as client } from '../fetch-B0tuycqO.cjs';
2
+ import { L as L402ClientOptions, T as TokenStore, c as L402Token } from '../types-FRMMn5ej.cjs';
3
+ import '@lnbot/sdk';
4
+ import 'express';
5
+
6
+ /** In-memory budget tracker with periodic resets. */
7
+ declare class Budget {
8
+ private spent;
9
+ private periodStart;
10
+ private readonly totalSats;
11
+ private readonly periodMs;
12
+ constructor(options: L402ClientOptions);
13
+ private maybeReset;
14
+ /** Throws L402BudgetExceededError if spending `price` would exceed the budget. */
15
+ check(price: number): void;
16
+ /** Record a successful payment. */
17
+ record(price: number): void;
18
+ }
19
+
20
+ /** Default in-memory token cache backed by a Map. */
21
+ declare class MemoryStore implements TokenStore {
22
+ private tokens;
23
+ get(url: string): Promise<L402Token | null>;
24
+ set(url: string, token: L402Token): Promise<void>;
25
+ delete(url: string): Promise<void>;
26
+ }
27
+ /** No-op store — never caches, every request pays fresh. */
28
+ declare class NoStore implements TokenStore {
29
+ get(): Promise<null>;
30
+ set(): Promise<void>;
31
+ delete(): Promise<void>;
32
+ }
33
+ /** Resolve the `store` option into a concrete TokenStore. */
34
+ declare function resolveStore(store?: "memory" | "none" | TokenStore): TokenStore;
35
+
36
+ export { Budget, MemoryStore, NoStore, resolveStore };
@@ -0,0 +1,36 @@
1
+ export { L as L402Client, c as client } from '../fetch-BPQpEg8M.js';
2
+ import { L as L402ClientOptions, T as TokenStore, c as L402Token } from '../types-FRMMn5ej.js';
3
+ import '@lnbot/sdk';
4
+ import 'express';
5
+
6
+ /** In-memory budget tracker with periodic resets. */
7
+ declare class Budget {
8
+ private spent;
9
+ private periodStart;
10
+ private readonly totalSats;
11
+ private readonly periodMs;
12
+ constructor(options: L402ClientOptions);
13
+ private maybeReset;
14
+ /** Throws L402BudgetExceededError if spending `price` would exceed the budget. */
15
+ check(price: number): void;
16
+ /** Record a successful payment. */
17
+ record(price: number): void;
18
+ }
19
+
20
+ /** Default in-memory token cache backed by a Map. */
21
+ declare class MemoryStore implements TokenStore {
22
+ private tokens;
23
+ get(url: string): Promise<L402Token | null>;
24
+ set(url: string, token: L402Token): Promise<void>;
25
+ delete(url: string): Promise<void>;
26
+ }
27
+ /** No-op store — never caches, every request pays fresh. */
28
+ declare class NoStore implements TokenStore {
29
+ get(): Promise<null>;
30
+ set(): Promise<void>;
31
+ delete(): Promise<void>;
32
+ }
33
+ /** Resolve the `store` option into a concrete TokenStore. */
34
+ declare function resolveStore(store?: "memory" | "none" | TokenStore): TokenStore;
35
+
36
+ export { Budget, MemoryStore, NoStore, resolveStore };