@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.
@@ -0,0 +1,27 @@
1
+ import { p as paywall, a as parseAuthorization, b as parseChallenge, f as formatAuthorization, c as formatChallenge } from './headers-ByStTJM9.cjs';
2
+ import { c as client } from './fetch-B0tuycqO.cjs';
3
+ export { L as L402Client } from './fetch-B0tuycqO.cjs';
4
+ export { L as L402ClientOptions, a as L402PaywallOptions, b as L402RequestData, c as L402Token, T as TokenStore } from './types-FRMMn5ej.cjs';
5
+ export { LnBot } from '@lnbot/sdk';
6
+ import 'express';
7
+
8
+ declare class L402Error extends Error {
9
+ constructor(message: string);
10
+ }
11
+ declare class L402BudgetExceededError extends L402Error {
12
+ constructor(message: string);
13
+ }
14
+ declare class L402PaymentFailedError extends L402Error {
15
+ constructor(message: string);
16
+ }
17
+
18
+ declare const l402: {
19
+ paywall: typeof paywall;
20
+ client: typeof client;
21
+ parseAuthorization: typeof parseAuthorization;
22
+ parseChallenge: typeof parseChallenge;
23
+ formatAuthorization: typeof formatAuthorization;
24
+ formatChallenge: typeof formatChallenge;
25
+ };
26
+
27
+ export { L402BudgetExceededError, L402Error, L402PaymentFailedError, l402 };
@@ -0,0 +1,27 @@
1
+ import { p as paywall, a as parseAuthorization, b as parseChallenge, f as formatAuthorization, c as formatChallenge } from './headers-BjcT_Am-.js';
2
+ import { c as client } from './fetch-BPQpEg8M.js';
3
+ export { L as L402Client } from './fetch-BPQpEg8M.js';
4
+ export { L as L402ClientOptions, a as L402PaywallOptions, b as L402RequestData, c as L402Token, T as TokenStore } from './types-FRMMn5ej.js';
5
+ export { LnBot } from '@lnbot/sdk';
6
+ import 'express';
7
+
8
+ declare class L402Error extends Error {
9
+ constructor(message: string);
10
+ }
11
+ declare class L402BudgetExceededError extends L402Error {
12
+ constructor(message: string);
13
+ }
14
+ declare class L402PaymentFailedError extends L402Error {
15
+ constructor(message: string);
16
+ }
17
+
18
+ declare const l402: {
19
+ paywall: typeof paywall;
20
+ client: typeof client;
21
+ parseAuthorization: typeof parseAuthorization;
22
+ parseChallenge: typeof parseChallenge;
23
+ formatAuthorization: typeof formatAuthorization;
24
+ formatChallenge: typeof formatChallenge;
25
+ };
26
+
27
+ export { L402BudgetExceededError, L402Error, L402PaymentFailedError, l402 };
package/dist/index.js ADDED
@@ -0,0 +1,260 @@
1
+ // src/server/pricing.ts
2
+ async function resolvePrice(price, req) {
3
+ return typeof price === "function" ? price(req) : price;
4
+ }
5
+
6
+ // src/server/middleware.ts
7
+ function paywall(ln, options) {
8
+ return async (req, res, next) => {
9
+ const authHeader = req.headers["authorization"];
10
+ if (authHeader && authHeader.startsWith("L402 ")) {
11
+ try {
12
+ const result = await ln.l402.verify({ authorization: authHeader });
13
+ if (result.valid) {
14
+ req.l402 = {
15
+ paymentHash: result.paymentHash,
16
+ caveats: result.caveats
17
+ };
18
+ return next();
19
+ }
20
+ } catch {
21
+ }
22
+ }
23
+ const price = await resolvePrice(options.price, req);
24
+ try {
25
+ const challenge = await ln.l402.createChallenge({
26
+ amount: price,
27
+ description: options.description,
28
+ expirySeconds: options.expirySeconds,
29
+ caveats: options.caveats
30
+ });
31
+ res.status(402).header("WWW-Authenticate", challenge.wwwAuthenticate).json({
32
+ type: "payment_required",
33
+ title: "Payment Required",
34
+ detail: "Pay the included Lightning invoice to access this resource.",
35
+ invoice: challenge.invoice,
36
+ macaroon: challenge.macaroon,
37
+ price,
38
+ unit: "satoshis",
39
+ description: options.description
40
+ });
41
+ } catch (err) {
42
+ next(err);
43
+ }
44
+ };
45
+ }
46
+
47
+ // src/server/headers.ts
48
+ function parseAuthorization(header) {
49
+ if (!header.startsWith("L402 ")) return null;
50
+ const token = header.slice(5);
51
+ const colonIndex = token.lastIndexOf(":");
52
+ if (colonIndex === -1) return null;
53
+ return {
54
+ macaroon: token.slice(0, colonIndex),
55
+ preimage: token.slice(colonIndex + 1)
56
+ };
57
+ }
58
+ function parseChallenge(header) {
59
+ if (!header.startsWith("L402 ")) return null;
60
+ const macaroonMatch = header.match(/macaroon="([^"]+)"/);
61
+ const invoiceMatch = header.match(/invoice="([^"]+)"/);
62
+ if (!macaroonMatch || !invoiceMatch) return null;
63
+ return { macaroon: macaroonMatch[1], invoice: invoiceMatch[1] };
64
+ }
65
+ function formatAuthorization(macaroon, preimage) {
66
+ return `L402 ${macaroon}:${preimage}`;
67
+ }
68
+ function formatChallenge(macaroon, invoice) {
69
+ return `L402 macaroon="${macaroon}", invoice="${invoice}"`;
70
+ }
71
+
72
+ // src/errors.ts
73
+ var L402Error = class extends Error {
74
+ constructor(message) {
75
+ super(message);
76
+ this.name = "L402Error";
77
+ }
78
+ };
79
+ var L402BudgetExceededError = class extends L402Error {
80
+ constructor(message) {
81
+ super(message);
82
+ this.name = "L402BudgetExceededError";
83
+ }
84
+ };
85
+ var L402PaymentFailedError = class extends L402Error {
86
+ constructor(message) {
87
+ super(message);
88
+ this.name = "L402PaymentFailedError";
89
+ }
90
+ };
91
+
92
+ // src/client/budget.ts
93
+ var PERIOD_MS = {
94
+ hour: 60 * 60 * 1e3,
95
+ day: 24 * 60 * 60 * 1e3,
96
+ week: 7 * 24 * 60 * 60 * 1e3,
97
+ month: 30 * 24 * 60 * 60 * 1e3
98
+ };
99
+ var Budget = class {
100
+ spent = 0;
101
+ periodStart = Date.now();
102
+ totalSats;
103
+ periodMs;
104
+ constructor(options) {
105
+ this.totalSats = options.budgetSats;
106
+ if (options.budgetPeriod) {
107
+ this.periodMs = PERIOD_MS[options.budgetPeriod];
108
+ }
109
+ }
110
+ maybeReset() {
111
+ if (this.periodMs && Date.now() - this.periodStart >= this.periodMs) {
112
+ this.spent = 0;
113
+ this.periodStart = Date.now();
114
+ }
115
+ }
116
+ /** Throws L402BudgetExceededError if spending `price` would exceed the budget. */
117
+ check(price) {
118
+ if (this.totalSats === void 0) return;
119
+ this.maybeReset();
120
+ if (this.spent + price > this.totalSats) {
121
+ throw new L402BudgetExceededError(
122
+ `Payment of ${price} sats would exceed budget (${this.spent}/${this.totalSats} sats spent)`
123
+ );
124
+ }
125
+ }
126
+ /** Record a successful payment. */
127
+ record(price) {
128
+ this.maybeReset();
129
+ this.spent += price;
130
+ }
131
+ };
132
+
133
+ // src/client/store.ts
134
+ function normalizeUrl(url) {
135
+ try {
136
+ const u = new URL(url);
137
+ u.search = "";
138
+ u.hash = "";
139
+ return u.toString().replace(/\/+$/, "");
140
+ } catch {
141
+ return url;
142
+ }
143
+ }
144
+ var MemoryStore = class {
145
+ tokens = /* @__PURE__ */ new Map();
146
+ async get(url) {
147
+ return this.tokens.get(normalizeUrl(url)) ?? null;
148
+ }
149
+ async set(url, token) {
150
+ this.tokens.set(normalizeUrl(url), token);
151
+ }
152
+ async delete(url) {
153
+ this.tokens.delete(normalizeUrl(url));
154
+ }
155
+ };
156
+ var NoStore = class {
157
+ async get() {
158
+ return null;
159
+ }
160
+ async set() {
161
+ }
162
+ async delete() {
163
+ }
164
+ };
165
+ function resolveStore(store) {
166
+ if (!store || store === "memory") return new MemoryStore();
167
+ if (store === "none") return new NoStore();
168
+ return store;
169
+ }
170
+
171
+ // src/client/fetch.ts
172
+ function client(ln, options = {}) {
173
+ const store = resolveStore(options.store);
174
+ const budget = new Budget(options);
175
+ const maxPrice = options.maxPrice ?? 1e3;
176
+ async function l402Fetch(url, init) {
177
+ const cached = await store.get(url);
178
+ if (cached) {
179
+ const isExpired = cached.expiresAt && cached.expiresAt.getTime() <= Date.now();
180
+ if (!isExpired) {
181
+ const headers = new Headers(init?.headers);
182
+ headers.set("Authorization", cached.authorization);
183
+ const res2 = await globalThis.fetch(url, { ...init, headers });
184
+ if (res2.status !== 402) return res2;
185
+ await store.delete(url);
186
+ } else {
187
+ await store.delete(url);
188
+ }
189
+ }
190
+ const res = await globalThis.fetch(url, init);
191
+ if (res.status !== 402) return res;
192
+ const wwwAuth = res.headers.get("www-authenticate");
193
+ if (!wwwAuth)
194
+ throw new L402Error("402 response missing WWW-Authenticate header");
195
+ const challenge = parseChallenge(wwwAuth);
196
+ if (!challenge) throw new L402Error("Could not parse L402 challenge");
197
+ const body = await res.json().catch(() => null);
198
+ const price = body?.price ?? 0;
199
+ if (price > maxPrice) {
200
+ throw new L402Error(
201
+ `Price ${price} sats exceeds maxPrice ${maxPrice}`
202
+ );
203
+ }
204
+ budget.check(price);
205
+ const payment = await ln.l402.pay({ wwwAuthenticate: wwwAuth });
206
+ if (payment.status === "failed") {
207
+ throw new L402PaymentFailedError("L402 payment failed");
208
+ }
209
+ if (!payment.authorization) {
210
+ throw new L402PaymentFailedError(
211
+ "Payment did not return authorization token"
212
+ );
213
+ }
214
+ const parsed = parseAuthorization(payment.authorization);
215
+ await store.set(url, {
216
+ macaroon: parsed?.macaroon ?? challenge.macaroon,
217
+ preimage: payment.preimage ?? parsed?.preimage ?? "",
218
+ authorization: payment.authorization,
219
+ paidAt: /* @__PURE__ */ new Date(),
220
+ expiresAt: body?.expiresAt ? new Date(body.expiresAt) : void 0
221
+ });
222
+ budget.record(price);
223
+ const retryHeaders = new Headers(init?.headers);
224
+ retryHeaders.set("Authorization", payment.authorization);
225
+ return globalThis.fetch(url, { ...init, headers: retryHeaders });
226
+ }
227
+ return {
228
+ fetch: l402Fetch,
229
+ async get(url, init) {
230
+ const res = await l402Fetch(url, { ...init, method: "GET" });
231
+ return res.json();
232
+ },
233
+ async post(url, init) {
234
+ const res = await l402Fetch(url, { ...init, method: "POST" });
235
+ return res.json();
236
+ }
237
+ };
238
+ }
239
+
240
+ // src/index.ts
241
+ import { LnBot } from "@lnbot/sdk";
242
+ var l402 = {
243
+ // Server
244
+ paywall,
245
+ // Client
246
+ client,
247
+ // Header utilities (for custom integrations)
248
+ parseAuthorization,
249
+ parseChallenge,
250
+ formatAuthorization,
251
+ formatChallenge
252
+ };
253
+ export {
254
+ L402BudgetExceededError,
255
+ L402Error,
256
+ L402PaymentFailedError,
257
+ LnBot,
258
+ l402
259
+ };
260
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/pricing.ts","../src/server/middleware.ts","../src/server/headers.ts","../src/errors.ts","../src/client/budget.ts","../src/client/store.ts","../src/client/fetch.ts","../src/index.ts"],"sourcesContent":["import type { Request } from \"express\";\n\nexport type PricingFn = (req: Request) => number | Promise<number>;\n\n/** Resolve price from a fixed number or a per-request pricing function. */\nexport async function resolvePrice(\n price: number | PricingFn,\n req: Request,\n): Promise<number> {\n return typeof price === \"function\" ? price(req) : price;\n}\n","import type { Request, Response, NextFunction } from \"express\";\nimport type { LnBot } from \"@lnbot/sdk\";\nimport type { L402PaywallOptions } from \"../types.js\";\nimport { resolvePrice } from \"./pricing.js\";\n\n/**\n * Express middleware factory that protects routes behind an L402 paywall.\n *\n * Two SDK calls:\n * - `ln.l402.verify()` — check an incoming Authorization header\n * - `ln.l402.createChallenge()` — mint a new invoice + macaroon challenge\n */\nexport function paywall(ln: LnBot, options: L402PaywallOptions) {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Step 1: Check for existing L402 Authorization header\n const authHeader = req.headers[\"authorization\"];\n\n if (authHeader && authHeader.startsWith(\"L402 \")) {\n // Step 2: Verify via SDK (stateless — checks signature, preimage, caveats)\n try {\n const result = await ln.l402.verify({ authorization: authHeader });\n\n if (result.valid) {\n req.l402 = {\n paymentHash: result.paymentHash!,\n caveats: result.caveats,\n };\n return next();\n }\n } catch {\n // Verification failed or errored — fall through to issue new challenge\n }\n }\n\n // Step 3: No valid token — create a challenge via SDK\n const price = await resolvePrice(options.price, req);\n\n try {\n const challenge = await ln.l402.createChallenge({\n amount: price,\n description: options.description,\n expirySeconds: options.expirySeconds,\n caveats: options.caveats,\n });\n\n // Step 4: Return 402 with challenge\n res\n .status(402)\n .header(\"WWW-Authenticate\", challenge.wwwAuthenticate)\n .json({\n type: \"payment_required\",\n title: \"Payment Required\",\n detail:\n \"Pay the included Lightning invoice to access this resource.\",\n invoice: challenge.invoice,\n macaroon: challenge.macaroon,\n price,\n unit: \"satoshis\",\n description: options.description,\n });\n } catch (err) {\n next(err);\n }\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","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","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","import { paywall } from \"./server/middleware.js\";\nimport {\n parseAuthorization,\n parseChallenge,\n formatAuthorization,\n formatChallenge,\n} from \"./server/headers.js\";\nimport { client } from \"./client/fetch.js\";\n\nexport const l402 = {\n // Server\n paywall,\n\n // Client\n client,\n\n // Header utilities (for custom integrations)\n parseAuthorization,\n parseChallenge,\n formatAuthorization,\n formatChallenge,\n};\n\nexport type {\n L402PaywallOptions,\n L402ClientOptions,\n L402Token,\n TokenStore,\n L402RequestData,\n} from \"./types.js\";\n\nexport type { L402Client } from \"./client/fetch.js\";\n\nexport { L402Error, L402BudgetExceededError, L402PaymentFailedError } from \"./errors.js\";\n\nexport { LnBot } from \"@lnbot/sdk\";\n"],"mappings":"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","names":["res"]}
@@ -0,0 +1,111 @@
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/server/index.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ formatAuthorization: () => formatAuthorization,
24
+ formatChallenge: () => formatChallenge,
25
+ parseAuthorization: () => parseAuthorization,
26
+ parseChallenge: () => parseChallenge,
27
+ paywall: () => paywall,
28
+ resolvePrice: () => resolvePrice
29
+ });
30
+ module.exports = __toCommonJS(server_exports);
31
+
32
+ // src/server/pricing.ts
33
+ async function resolvePrice(price, req) {
34
+ return typeof price === "function" ? price(req) : price;
35
+ }
36
+
37
+ // src/server/middleware.ts
38
+ function paywall(ln, options) {
39
+ return async (req, res, next) => {
40
+ const authHeader = req.headers["authorization"];
41
+ if (authHeader && authHeader.startsWith("L402 ")) {
42
+ try {
43
+ const result = await ln.l402.verify({ authorization: authHeader });
44
+ if (result.valid) {
45
+ req.l402 = {
46
+ paymentHash: result.paymentHash,
47
+ caveats: result.caveats
48
+ };
49
+ return next();
50
+ }
51
+ } catch {
52
+ }
53
+ }
54
+ const price = await resolvePrice(options.price, req);
55
+ try {
56
+ const challenge = await ln.l402.createChallenge({
57
+ amount: price,
58
+ description: options.description,
59
+ expirySeconds: options.expirySeconds,
60
+ caveats: options.caveats
61
+ });
62
+ res.status(402).header("WWW-Authenticate", challenge.wwwAuthenticate).json({
63
+ type: "payment_required",
64
+ title: "Payment Required",
65
+ detail: "Pay the included Lightning invoice to access this resource.",
66
+ invoice: challenge.invoice,
67
+ macaroon: challenge.macaroon,
68
+ price,
69
+ unit: "satoshis",
70
+ description: options.description
71
+ });
72
+ } catch (err) {
73
+ next(err);
74
+ }
75
+ };
76
+ }
77
+
78
+ // src/server/headers.ts
79
+ function parseAuthorization(header) {
80
+ if (!header.startsWith("L402 ")) return null;
81
+ const token = header.slice(5);
82
+ const colonIndex = token.lastIndexOf(":");
83
+ if (colonIndex === -1) return null;
84
+ return {
85
+ macaroon: token.slice(0, colonIndex),
86
+ preimage: token.slice(colonIndex + 1)
87
+ };
88
+ }
89
+ function parseChallenge(header) {
90
+ if (!header.startsWith("L402 ")) return null;
91
+ const macaroonMatch = header.match(/macaroon="([^"]+)"/);
92
+ const invoiceMatch = header.match(/invoice="([^"]+)"/);
93
+ if (!macaroonMatch || !invoiceMatch) return null;
94
+ return { macaroon: macaroonMatch[1], invoice: invoiceMatch[1] };
95
+ }
96
+ function formatAuthorization(macaroon, preimage) {
97
+ return `L402 ${macaroon}:${preimage}`;
98
+ }
99
+ function formatChallenge(macaroon, invoice) {
100
+ return `L402 macaroon="${macaroon}", invoice="${invoice}"`;
101
+ }
102
+ // Annotate the CommonJS export names for ESM import in node:
103
+ 0 && (module.exports = {
104
+ formatAuthorization,
105
+ formatChallenge,
106
+ parseAuthorization,
107
+ parseChallenge,
108
+ paywall,
109
+ resolvePrice
110
+ });
111
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/index.ts","../../src/server/pricing.ts","../../src/server/middleware.ts","../../src/server/headers.ts"],"sourcesContent":["export { paywall } from \"./middleware.js\";\nexport {\n parseAuthorization,\n parseChallenge,\n formatAuthorization,\n formatChallenge,\n} from \"./headers.js\";\nexport { resolvePrice, type PricingFn } from \"./pricing.js\";\n","import type { Request } from \"express\";\n\nexport type PricingFn = (req: Request) => number | Promise<number>;\n\n/** Resolve price from a fixed number or a per-request pricing function. */\nexport async function resolvePrice(\n price: number | PricingFn,\n req: Request,\n): Promise<number> {\n return typeof price === \"function\" ? price(req) : price;\n}\n","import type { Request, Response, NextFunction } from \"express\";\nimport type { LnBot } from \"@lnbot/sdk\";\nimport type { L402PaywallOptions } from \"../types.js\";\nimport { resolvePrice } from \"./pricing.js\";\n\n/**\n * Express middleware factory that protects routes behind an L402 paywall.\n *\n * Two SDK calls:\n * - `ln.l402.verify()` — check an incoming Authorization header\n * - `ln.l402.createChallenge()` — mint a new invoice + macaroon challenge\n */\nexport function paywall(ln: LnBot, options: L402PaywallOptions) {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Step 1: Check for existing L402 Authorization header\n const authHeader = req.headers[\"authorization\"];\n\n if (authHeader && authHeader.startsWith(\"L402 \")) {\n // Step 2: Verify via SDK (stateless — checks signature, preimage, caveats)\n try {\n const result = await ln.l402.verify({ authorization: authHeader });\n\n if (result.valid) {\n req.l402 = {\n paymentHash: result.paymentHash!,\n caveats: result.caveats,\n };\n return next();\n }\n } catch {\n // Verification failed or errored — fall through to issue new challenge\n }\n }\n\n // Step 3: No valid token — create a challenge via SDK\n const price = await resolvePrice(options.price, req);\n\n try {\n const challenge = await ln.l402.createChallenge({\n amount: price,\n description: options.description,\n expirySeconds: options.expirySeconds,\n caveats: options.caveats,\n });\n\n // Step 4: Return 402 with challenge\n res\n .status(402)\n .header(\"WWW-Authenticate\", challenge.wwwAuthenticate)\n .json({\n type: \"payment_required\",\n title: \"Payment Required\",\n detail:\n \"Pay the included Lightning invoice to access this resource.\",\n invoice: challenge.invoice,\n macaroon: challenge.macaroon,\n price,\n unit: \"satoshis\",\n description: options.description,\n });\n } catch (err) {\n next(err);\n }\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,eAAsB,aACpB,OACA,KACiB;AACjB,SAAO,OAAO,UAAU,aAAa,MAAM,GAAG,IAAI;AACpD;;;ACEO,SAAS,QAAQ,IAAW,SAA6B;AAC9D,SAAO,OAAO,KAAc,KAAe,SAAuB;AAEhE,UAAM,aAAa,IAAI,QAAQ,eAAe;AAE9C,QAAI,cAAc,WAAW,WAAW,OAAO,GAAG;AAEhD,UAAI;AACF,cAAM,SAAS,MAAM,GAAG,KAAK,OAAO,EAAE,eAAe,WAAW,CAAC;AAEjE,YAAI,OAAO,OAAO;AAChB,cAAI,OAAO;AAAA,YACT,aAAa,OAAO;AAAA,YACpB,SAAS,OAAO;AAAA,UAClB;AACA,iBAAO,KAAK;AAAA,QACd;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,UAAM,QAAQ,MAAM,aAAa,QAAQ,OAAO,GAAG;AAEnD,QAAI;AACF,YAAM,YAAY,MAAM,GAAG,KAAK,gBAAgB;AAAA,QAC9C,QAAQ;AAAA,QACR,aAAa,QAAQ;AAAA,QACrB,eAAe,QAAQ;AAAA,QACvB,SAAS,QAAQ;AAAA,MACnB,CAAC;AAGD,UACG,OAAO,GAAG,EACV,OAAO,oBAAoB,UAAU,eAAe,EACpD,KAAK;AAAA,QACJ,MAAM;AAAA,QACN,OAAO;AAAA,QACP,QACE;AAAA,QACF,SAAS,UAAU;AAAA,QACnB,UAAU,UAAU;AAAA,QACpB;AAAA,QACA,MAAM;AAAA,QACN,aAAa,QAAQ;AAAA,MACvB,CAAC;AAAA,IACL,SAAS,KAAK;AACZ,WAAK,GAAG;AAAA,IACV;AAAA,EACF;AACF;;;AC/DO,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;AAGO,SAAS,oBACd,UACA,UACQ;AACR,SAAO,QAAQ,QAAQ,IAAI,QAAQ;AACrC;AAGO,SAAS,gBACd,UACA,SACQ;AACR,SAAO,kBAAkB,QAAQ,eAAe,OAAO;AACzD;","names":[]}
@@ -0,0 +1,10 @@
1
+ export { f as formatAuthorization, c as formatChallenge, a as parseAuthorization, b as parseChallenge, p as paywall } from '../headers-ByStTJM9.cjs';
2
+ import { Request } from 'express';
3
+ import '@lnbot/sdk';
4
+ import '../types-FRMMn5ej.cjs';
5
+
6
+ type PricingFn = (req: Request) => number | Promise<number>;
7
+ /** Resolve price from a fixed number or a per-request pricing function. */
8
+ declare function resolvePrice(price: number | PricingFn, req: Request): Promise<number>;
9
+
10
+ export { type PricingFn, resolvePrice };
@@ -0,0 +1,10 @@
1
+ export { f as formatAuthorization, c as formatChallenge, a as parseAuthorization, b as parseChallenge, p as paywall } from '../headers-BjcT_Am-.js';
2
+ import { Request } from 'express';
3
+ import '@lnbot/sdk';
4
+ import '../types-FRMMn5ej.js';
5
+
6
+ type PricingFn = (req: Request) => number | Promise<number>;
7
+ /** Resolve price from a fixed number or a per-request pricing function. */
8
+ declare function resolvePrice(price: number | PricingFn, req: Request): Promise<number>;
9
+
10
+ export { type PricingFn, resolvePrice };
@@ -0,0 +1,79 @@
1
+ // src/server/pricing.ts
2
+ async function resolvePrice(price, req) {
3
+ return typeof price === "function" ? price(req) : price;
4
+ }
5
+
6
+ // src/server/middleware.ts
7
+ function paywall(ln, options) {
8
+ return async (req, res, next) => {
9
+ const authHeader = req.headers["authorization"];
10
+ if (authHeader && authHeader.startsWith("L402 ")) {
11
+ try {
12
+ const result = await ln.l402.verify({ authorization: authHeader });
13
+ if (result.valid) {
14
+ req.l402 = {
15
+ paymentHash: result.paymentHash,
16
+ caveats: result.caveats
17
+ };
18
+ return next();
19
+ }
20
+ } catch {
21
+ }
22
+ }
23
+ const price = await resolvePrice(options.price, req);
24
+ try {
25
+ const challenge = await ln.l402.createChallenge({
26
+ amount: price,
27
+ description: options.description,
28
+ expirySeconds: options.expirySeconds,
29
+ caveats: options.caveats
30
+ });
31
+ res.status(402).header("WWW-Authenticate", challenge.wwwAuthenticate).json({
32
+ type: "payment_required",
33
+ title: "Payment Required",
34
+ detail: "Pay the included Lightning invoice to access this resource.",
35
+ invoice: challenge.invoice,
36
+ macaroon: challenge.macaroon,
37
+ price,
38
+ unit: "satoshis",
39
+ description: options.description
40
+ });
41
+ } catch (err) {
42
+ next(err);
43
+ }
44
+ };
45
+ }
46
+
47
+ // src/server/headers.ts
48
+ function parseAuthorization(header) {
49
+ if (!header.startsWith("L402 ")) return null;
50
+ const token = header.slice(5);
51
+ const colonIndex = token.lastIndexOf(":");
52
+ if (colonIndex === -1) return null;
53
+ return {
54
+ macaroon: token.slice(0, colonIndex),
55
+ preimage: token.slice(colonIndex + 1)
56
+ };
57
+ }
58
+ function parseChallenge(header) {
59
+ if (!header.startsWith("L402 ")) return null;
60
+ const macaroonMatch = header.match(/macaroon="([^"]+)"/);
61
+ const invoiceMatch = header.match(/invoice="([^"]+)"/);
62
+ if (!macaroonMatch || !invoiceMatch) return null;
63
+ return { macaroon: macaroonMatch[1], invoice: invoiceMatch[1] };
64
+ }
65
+ function formatAuthorization(macaroon, preimage) {
66
+ return `L402 ${macaroon}:${preimage}`;
67
+ }
68
+ function formatChallenge(macaroon, invoice) {
69
+ return `L402 macaroon="${macaroon}", invoice="${invoice}"`;
70
+ }
71
+ export {
72
+ formatAuthorization,
73
+ formatChallenge,
74
+ parseAuthorization,
75
+ parseChallenge,
76
+ paywall,
77
+ resolvePrice
78
+ };
79
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/pricing.ts","../../src/server/middleware.ts","../../src/server/headers.ts"],"sourcesContent":["import type { Request } from \"express\";\n\nexport type PricingFn = (req: Request) => number | Promise<number>;\n\n/** Resolve price from a fixed number or a per-request pricing function. */\nexport async function resolvePrice(\n price: number | PricingFn,\n req: Request,\n): Promise<number> {\n return typeof price === \"function\" ? price(req) : price;\n}\n","import type { Request, Response, NextFunction } from \"express\";\nimport type { LnBot } from \"@lnbot/sdk\";\nimport type { L402PaywallOptions } from \"../types.js\";\nimport { resolvePrice } from \"./pricing.js\";\n\n/**\n * Express middleware factory that protects routes behind an L402 paywall.\n *\n * Two SDK calls:\n * - `ln.l402.verify()` — check an incoming Authorization header\n * - `ln.l402.createChallenge()` — mint a new invoice + macaroon challenge\n */\nexport function paywall(ln: LnBot, options: L402PaywallOptions) {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Step 1: Check for existing L402 Authorization header\n const authHeader = req.headers[\"authorization\"];\n\n if (authHeader && authHeader.startsWith(\"L402 \")) {\n // Step 2: Verify via SDK (stateless — checks signature, preimage, caveats)\n try {\n const result = await ln.l402.verify({ authorization: authHeader });\n\n if (result.valid) {\n req.l402 = {\n paymentHash: result.paymentHash!,\n caveats: result.caveats,\n };\n return next();\n }\n } catch {\n // Verification failed or errored — fall through to issue new challenge\n }\n }\n\n // Step 3: No valid token — create a challenge via SDK\n const price = await resolvePrice(options.price, req);\n\n try {\n const challenge = await ln.l402.createChallenge({\n amount: price,\n description: options.description,\n expirySeconds: options.expirySeconds,\n caveats: options.caveats,\n });\n\n // Step 4: Return 402 with challenge\n res\n .status(402)\n .header(\"WWW-Authenticate\", challenge.wwwAuthenticate)\n .json({\n type: \"payment_required\",\n title: \"Payment Required\",\n detail:\n \"Pay the included Lightning invoice to access this resource.\",\n invoice: challenge.invoice,\n macaroon: challenge.macaroon,\n price,\n unit: \"satoshis\",\n description: options.description,\n });\n } catch (err) {\n next(err);\n }\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"],"mappings":";AAKA,eAAsB,aACpB,OACA,KACiB;AACjB,SAAO,OAAO,UAAU,aAAa,MAAM,GAAG,IAAI;AACpD;;;ACEO,SAAS,QAAQ,IAAW,SAA6B;AAC9D,SAAO,OAAO,KAAc,KAAe,SAAuB;AAEhE,UAAM,aAAa,IAAI,QAAQ,eAAe;AAE9C,QAAI,cAAc,WAAW,WAAW,OAAO,GAAG;AAEhD,UAAI;AACF,cAAM,SAAS,MAAM,GAAG,KAAK,OAAO,EAAE,eAAe,WAAW,CAAC;AAEjE,YAAI,OAAO,OAAO;AAChB,cAAI,OAAO;AAAA,YACT,aAAa,OAAO;AAAA,YACpB,SAAS,OAAO;AAAA,UAClB;AACA,iBAAO,KAAK;AAAA,QACd;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,UAAM,QAAQ,MAAM,aAAa,QAAQ,OAAO,GAAG;AAEnD,QAAI;AACF,YAAM,YAAY,MAAM,GAAG,KAAK,gBAAgB;AAAA,QAC9C,QAAQ;AAAA,QACR,aAAa,QAAQ;AAAA,QACrB,eAAe,QAAQ;AAAA,QACvB,SAAS,QAAQ;AAAA,MACnB,CAAC;AAGD,UACG,OAAO,GAAG,EACV,OAAO,oBAAoB,UAAU,eAAe,EACpD,KAAK;AAAA,QACJ,MAAM;AAAA,QACN,OAAO;AAAA,QACP,QACE;AAAA,QACF,SAAS,UAAU;AAAA,QACnB,UAAU,UAAU;AAAA,QACpB;AAAA,QACA,MAAM;AAAA,QACN,aAAa,QAAQ;AAAA,MACvB,CAAC;AAAA,IACL,SAAS,KAAK;AACZ,WAAK,GAAG;AAAA,IACV;AAAA,EACF;AACF;;;AC/DO,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;AAGO,SAAS,oBACd,UACA,UACQ;AACR,SAAO,QAAQ,QAAQ,IAAI,QAAQ;AACrC;AAGO,SAAS,gBACd,UACA,SACQ;AACR,SAAO,kBAAkB,QAAQ,eAAe,OAAO;AACzD;","names":[]}
@@ -0,0 +1,52 @@
1
+ import { Request } from 'express';
2
+
3
+ /** Data attached to `req.l402` after successful L402 verification. */
4
+ interface L402RequestData {
5
+ paymentHash: string;
6
+ caveats: string[] | null;
7
+ }
8
+ declare global {
9
+ namespace Express {
10
+ interface Request {
11
+ l402?: L402RequestData;
12
+ }
13
+ }
14
+ }
15
+ /** Options for the `l402.paywall()` middleware. */
16
+ interface L402PaywallOptions {
17
+ /** Price in satoshis — fixed number or async function receiving the request. */
18
+ price: number | ((req: Request) => number | Promise<number>);
19
+ /** Invoice memo / description. */
20
+ description?: string;
21
+ /** Challenge expiry in seconds. */
22
+ expirySeconds?: number;
23
+ /** Macaroon caveats to attach. */
24
+ caveats?: string[];
25
+ }
26
+ /** Options for the `l402.client()` factory. */
27
+ interface L402ClientOptions {
28
+ /** Max sats to pay for a single request (default: 1000). */
29
+ maxPrice?: number;
30
+ /** Total budget in sats for the period. */
31
+ budgetSats?: number;
32
+ /** Budget reset period. */
33
+ budgetPeriod?: "hour" | "day" | "week" | "month";
34
+ /** Token cache strategy (default: "memory"). */
35
+ store?: "memory" | "none" | TokenStore;
36
+ }
37
+ /** Pluggable token cache. */
38
+ interface TokenStore {
39
+ get(url: string): Promise<L402Token | null>;
40
+ set(url: string, token: L402Token): Promise<void>;
41
+ delete(url: string): Promise<void>;
42
+ }
43
+ /** A cached L402 credential. */
44
+ interface L402Token {
45
+ macaroon: string;
46
+ preimage: string;
47
+ authorization: string;
48
+ paidAt: Date;
49
+ expiresAt?: Date;
50
+ }
51
+
52
+ export type { L402ClientOptions as L, TokenStore as T, L402PaywallOptions as a, L402RequestData as b, L402Token as c };