@lnbot/l402 0.1.0 → 0.2.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.
@@ -175,7 +175,7 @@ function client(ln, options = {}) {
175
175
  const body = await res.json().catch(() => null);
176
176
  const price = body?.price ?? 0;
177
177
  if (price > maxPrice) {
178
- throw new L402Error(
178
+ throw new L402BudgetExceededError(
179
179
  `Price ${price} sats exceeds maxPrice ${maxPrice}`
180
180
  );
181
181
  }
@@ -197,21 +197,28 @@ function client(ln, options = {}) {
197
197
  paidAt: /* @__PURE__ */ new Date(),
198
198
  expiresAt: body?.expiresAt ? new Date(body.expiresAt) : void 0
199
199
  });
200
- budget.record(price);
200
+ budget.record(payment.amount ?? price);
201
201
  const retryHeaders = new Headers(init?.headers);
202
202
  retryHeaders.set("Authorization", payment.authorization);
203
- return globalThis.fetch(url, { ...init, headers: retryHeaders });
203
+ const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });
204
+ if (retry.status === 402) {
205
+ throw new L402PaymentFailedError(
206
+ "Server returned 402 after successful payment"
207
+ );
208
+ }
209
+ return retry;
210
+ }
211
+ async function jsonMethod(method, url, init) {
212
+ const res = await l402Fetch(url, { ...init, method });
213
+ return res.json();
204
214
  }
205
215
  return {
206
216
  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
- }
217
+ get: (url, init) => jsonMethod("GET", url, init),
218
+ post: (url, init) => jsonMethod("POST", url, init),
219
+ put: (url, init) => jsonMethod("PUT", url, init),
220
+ patch: (url, init) => jsonMethod("PATCH", url, init),
221
+ delete: (url, init) => jsonMethod("DELETE", url, init)
215
222
  };
216
223
  }
217
224
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +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":"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","names":["res"]}
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 L402BudgetExceededError,\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 /** PUT + JSON parse with automatic L402 payment. */\n put(url: string, init?: RequestInit): Promise<unknown>;\n /** PATCH + JSON parse with automatic L402 payment. */\n patch(url: string, init?: RequestInit): Promise<unknown>;\n /** DELETE + JSON parse with automatic L402 payment. */\n delete(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 L402BudgetExceededError(\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(payment.amount ?? price);\n\n // Step 7: Retry with L402 Authorization\n const retryHeaders = new Headers(init?.headers);\n retryHeaders.set(\"Authorization\", payment.authorization);\n const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });\n\n if (retry.status === 402) {\n throw new L402PaymentFailedError(\n \"Server returned 402 after successful payment\",\n );\n }\n\n return retry;\n }\n\n async function jsonMethod(method: string, url: string, init?: RequestInit) {\n const res = await l402Fetch(url, { ...init, method });\n return res.json();\n }\n\n return {\n fetch: l402Fetch,\n get: (url: string, init?: RequestInit) => jsonMethod(\"GET\", url, init),\n post: (url: string, init?: RequestInit) => jsonMethod(\"POST\", url, init),\n put: (url: string, init?: RequestInit) => jsonMethod(\"PUT\", url, init),\n patch: (url: string, init?: RequestInit) => jsonMethod(\"PATCH\", url, init),\n delete: (url: string, init?: RequestInit) => jsonMethod(\"DELETE\", url, init),\n };\n}\n"],"mappings":"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","names":["res"]}
@@ -1,4 +1,4 @@
1
- export { L as L402Client, c as client } from '../fetch-B0tuycqO.cjs';
1
+ export { L as L402Client, c as client } from '../fetch-BbNwLZKo.cjs';
2
2
  import { L as L402ClientOptions, T as TokenStore, c as L402Token } from '../types-FRMMn5ej.cjs';
3
3
  import '@lnbot/sdk';
4
4
  import 'express';
@@ -1,4 +1,4 @@
1
- export { L as L402Client, c as client } from '../fetch-BPQpEg8M.js';
1
+ export { L as L402Client, c as client } from '../fetch-B2UA6hNH.js';
2
2
  import { L as L402ClientOptions, T as TokenStore, c as L402Token } from '../types-FRMMn5ej.js';
3
3
  import '@lnbot/sdk';
4
4
  import 'express';
@@ -145,7 +145,7 @@ function client(ln, options = {}) {
145
145
  const body = await res.json().catch(() => null);
146
146
  const price = body?.price ?? 0;
147
147
  if (price > maxPrice) {
148
- throw new L402Error(
148
+ throw new L402BudgetExceededError(
149
149
  `Price ${price} sats exceeds maxPrice ${maxPrice}`
150
150
  );
151
151
  }
@@ -167,21 +167,28 @@ function client(ln, options = {}) {
167
167
  paidAt: /* @__PURE__ */ new Date(),
168
168
  expiresAt: body?.expiresAt ? new Date(body.expiresAt) : void 0
169
169
  });
170
- budget.record(price);
170
+ budget.record(payment.amount ?? price);
171
171
  const retryHeaders = new Headers(init?.headers);
172
172
  retryHeaders.set("Authorization", payment.authorization);
173
- return globalThis.fetch(url, { ...init, headers: retryHeaders });
173
+ const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });
174
+ if (retry.status === 402) {
175
+ throw new L402PaymentFailedError(
176
+ "Server returned 402 after successful payment"
177
+ );
178
+ }
179
+ return retry;
180
+ }
181
+ async function jsonMethod(method, url, init) {
182
+ const res = await l402Fetch(url, { ...init, method });
183
+ return res.json();
174
184
  }
175
185
  return {
176
186
  fetch: l402Fetch,
177
- async get(url, init) {
178
- const res = await l402Fetch(url, { ...init, method: "GET" });
179
- return res.json();
180
- },
181
- async post(url, init) {
182
- const res = await l402Fetch(url, { ...init, method: "POST" });
183
- return res.json();
184
- }
187
+ get: (url, init) => jsonMethod("GET", url, init),
188
+ post: (url, init) => jsonMethod("POST", url, init),
189
+ put: (url, init) => jsonMethod("PUT", url, init),
190
+ patch: (url, init) => jsonMethod("PATCH", url, init),
191
+ delete: (url, init) => jsonMethod("DELETE", url, init)
185
192
  };
186
193
  }
187
194
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/errors.ts","../../src/server/headers.ts","../../src/client/budget.ts","../../src/client/store.ts","../../src/client/fetch.ts"],"sourcesContent":["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":"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","names":["res"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/server/headers.ts","../../src/client/budget.ts","../../src/client/store.ts","../../src/client/fetch.ts"],"sourcesContent":["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 L402BudgetExceededError,\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 /** PUT + JSON parse with automatic L402 payment. */\n put(url: string, init?: RequestInit): Promise<unknown>;\n /** PATCH + JSON parse with automatic L402 payment. */\n patch(url: string, init?: RequestInit): Promise<unknown>;\n /** DELETE + JSON parse with automatic L402 payment. */\n delete(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 L402BudgetExceededError(\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(payment.amount ?? price);\n\n // Step 7: Retry with L402 Authorization\n const retryHeaders = new Headers(init?.headers);\n retryHeaders.set(\"Authorization\", payment.authorization);\n const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });\n\n if (retry.status === 402) {\n throw new L402PaymentFailedError(\n \"Server returned 402 after successful payment\",\n );\n }\n\n return retry;\n }\n\n async function jsonMethod(method: string, url: string, init?: RequestInit) {\n const res = await l402Fetch(url, { ...init, method });\n return res.json();\n }\n\n return {\n fetch: l402Fetch,\n get: (url: string, init?: RequestInit) => jsonMethod(\"GET\", url, init),\n post: (url: string, init?: RequestInit) => jsonMethod(\"POST\", url, init),\n put: (url: string, init?: RequestInit) => jsonMethod(\"PUT\", url, init),\n patch: (url: string, init?: RequestInit) => jsonMethod(\"PATCH\", url, init),\n delete: (url: string, init?: RequestInit) => jsonMethod(\"DELETE\", url, init),\n };\n}\n"],"mappings":"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","names":["res"]}
@@ -9,6 +9,12 @@ interface L402Client {
9
9
  get(url: string, init?: RequestInit): Promise<unknown>;
10
10
  /** POST + JSON parse with automatic L402 payment. */
11
11
  post(url: string, init?: RequestInit): Promise<unknown>;
12
+ /** PUT + JSON parse with automatic L402 payment. */
13
+ put(url: string, init?: RequestInit): Promise<unknown>;
14
+ /** PATCH + JSON parse with automatic L402 payment. */
15
+ patch(url: string, init?: RequestInit): Promise<unknown>;
16
+ /** DELETE + JSON parse with automatic L402 payment. */
17
+ delete(url: string, init?: RequestInit): Promise<unknown>;
12
18
  }
13
19
  /**
14
20
  * Create an L402-aware HTTP client.
@@ -9,6 +9,12 @@ interface L402Client {
9
9
  get(url: string, init?: RequestInit): Promise<unknown>;
10
10
  /** POST + JSON parse with automatic L402 payment. */
11
11
  post(url: string, init?: RequestInit): Promise<unknown>;
12
+ /** PUT + JSON parse with automatic L402 payment. */
13
+ put(url: string, init?: RequestInit): Promise<unknown>;
14
+ /** PATCH + JSON parse with automatic L402 payment. */
15
+ patch(url: string, init?: RequestInit): Promise<unknown>;
16
+ /** DELETE + JSON parse with automatic L402 payment. */
17
+ delete(url: string, init?: RequestInit): Promise<unknown>;
12
18
  }
13
19
  /**
14
20
  * Create an L402-aware HTTP client.
package/dist/index.cjs CHANGED
@@ -227,7 +227,7 @@ function client(ln, options = {}) {
227
227
  const body = await res.json().catch(() => null);
228
228
  const price = body?.price ?? 0;
229
229
  if (price > maxPrice) {
230
- throw new L402Error(
230
+ throw new L402BudgetExceededError(
231
231
  `Price ${price} sats exceeds maxPrice ${maxPrice}`
232
232
  );
233
233
  }
@@ -249,21 +249,28 @@ function client(ln, options = {}) {
249
249
  paidAt: /* @__PURE__ */ new Date(),
250
250
  expiresAt: body?.expiresAt ? new Date(body.expiresAt) : void 0
251
251
  });
252
- budget.record(price);
252
+ budget.record(payment.amount ?? price);
253
253
  const retryHeaders = new Headers(init?.headers);
254
254
  retryHeaders.set("Authorization", payment.authorization);
255
- return globalThis.fetch(url, { ...init, headers: retryHeaders });
255
+ const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });
256
+ if (retry.status === 402) {
257
+ throw new L402PaymentFailedError(
258
+ "Server returned 402 after successful payment"
259
+ );
260
+ }
261
+ return retry;
262
+ }
263
+ async function jsonMethod(method, url, init) {
264
+ const res = await l402Fetch(url, { ...init, method });
265
+ return res.json();
256
266
  }
257
267
  return {
258
268
  fetch: l402Fetch,
259
- async get(url, init) {
260
- const res = await l402Fetch(url, { ...init, method: "GET" });
261
- return res.json();
262
- },
263
- async post(url, init) {
264
- const res = await l402Fetch(url, { ...init, method: "POST" });
265
- return res.json();
266
- }
269
+ get: (url, init) => jsonMethod("GET", url, init),
270
+ post: (url, init) => jsonMethod("POST", url, init),
271
+ put: (url, init) => jsonMethod("PUT", url, init),
272
+ patch: (url, init) => jsonMethod("PATCH", url, init),
273
+ delete: (url, init) => jsonMethod("DELETE", url, init)
267
274
  };
268
275
  }
269
276
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../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"],"sourcesContent":["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","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"],"mappings":"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","names":["res"]}
1
+ {"version":3,"sources":["../src/index.ts","../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"],"sourcesContent":["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","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 L402BudgetExceededError,\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 /** PUT + JSON parse with automatic L402 payment. */\n put(url: string, init?: RequestInit): Promise<unknown>;\n /** PATCH + JSON parse with automatic L402 payment. */\n patch(url: string, init?: RequestInit): Promise<unknown>;\n /** DELETE + JSON parse with automatic L402 payment. */\n delete(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 L402BudgetExceededError(\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(payment.amount ?? price);\n\n // Step 7: Retry with L402 Authorization\n const retryHeaders = new Headers(init?.headers);\n retryHeaders.set(\"Authorization\", payment.authorization);\n const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });\n\n if (retry.status === 402) {\n throw new L402PaymentFailedError(\n \"Server returned 402 after successful payment\",\n );\n }\n\n return retry;\n }\n\n async function jsonMethod(method: string, url: string, init?: RequestInit) {\n const res = await l402Fetch(url, { ...init, method });\n return res.json();\n }\n\n return {\n fetch: l402Fetch,\n get: (url: string, init?: RequestInit) => jsonMethod(\"GET\", url, init),\n post: (url: string, init?: RequestInit) => jsonMethod(\"POST\", url, init),\n put: (url: string, init?: RequestInit) => jsonMethod(\"PUT\", url, init),\n patch: (url: string, init?: RequestInit) => jsonMethod(\"PATCH\", url, init),\n delete: (url: string, init?: RequestInit) => jsonMethod(\"DELETE\", url, init),\n };\n}\n"],"mappings":"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","names":["res"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
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';
2
+ import { c as client } from './fetch-BbNwLZKo.cjs';
3
+ export { L as L402Client } from './fetch-BbNwLZKo.cjs';
4
4
  export { L as L402ClientOptions, a as L402PaywallOptions, b as L402RequestData, c as L402Token, T as TokenStore } from './types-FRMMn5ej.cjs';
5
5
  export { LnBot } from '@lnbot/sdk';
6
6
  import 'express';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
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';
2
+ import { c as client } from './fetch-B2UA6hNH.js';
3
+ export { L as L402Client } from './fetch-B2UA6hNH.js';
4
4
  export { L as L402ClientOptions, a as L402PaywallOptions, b as L402RequestData, c as L402Token, T as TokenStore } from './types-FRMMn5ej.js';
5
5
  export { LnBot } from '@lnbot/sdk';
6
6
  import 'express';
package/dist/index.js CHANGED
@@ -197,7 +197,7 @@ function client(ln, options = {}) {
197
197
  const body = await res.json().catch(() => null);
198
198
  const price = body?.price ?? 0;
199
199
  if (price > maxPrice) {
200
- throw new L402Error(
200
+ throw new L402BudgetExceededError(
201
201
  `Price ${price} sats exceeds maxPrice ${maxPrice}`
202
202
  );
203
203
  }
@@ -219,21 +219,28 @@ function client(ln, options = {}) {
219
219
  paidAt: /* @__PURE__ */ new Date(),
220
220
  expiresAt: body?.expiresAt ? new Date(body.expiresAt) : void 0
221
221
  });
222
- budget.record(price);
222
+ budget.record(payment.amount ?? price);
223
223
  const retryHeaders = new Headers(init?.headers);
224
224
  retryHeaders.set("Authorization", payment.authorization);
225
- return globalThis.fetch(url, { ...init, headers: retryHeaders });
225
+ const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });
226
+ if (retry.status === 402) {
227
+ throw new L402PaymentFailedError(
228
+ "Server returned 402 after successful payment"
229
+ );
230
+ }
231
+ return retry;
232
+ }
233
+ async function jsonMethod(method, url, init) {
234
+ const res = await l402Fetch(url, { ...init, method });
235
+ return res.json();
226
236
  }
227
237
  return {
228
238
  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
- }
239
+ get: (url, init) => jsonMethod("GET", url, init),
240
+ post: (url, init) => jsonMethod("POST", url, init),
241
+ put: (url, init) => jsonMethod("PUT", url, init),
242
+ patch: (url, init) => jsonMethod("PATCH", url, init),
243
+ delete: (url, init) => jsonMethod("DELETE", url, init)
237
244
  };
238
245
  }
239
246
 
package/dist/index.js.map CHANGED
@@ -1 +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"]}
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 L402BudgetExceededError,\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 /** PUT + JSON parse with automatic L402 payment. */\n put(url: string, init?: RequestInit): Promise<unknown>;\n /** PATCH + JSON parse with automatic L402 payment. */\n patch(url: string, init?: RequestInit): Promise<unknown>;\n /** DELETE + JSON parse with automatic L402 payment. */\n delete(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 L402BudgetExceededError(\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(payment.amount ?? price);\n\n // Step 7: Retry with L402 Authorization\n const retryHeaders = new Headers(init?.headers);\n retryHeaders.set(\"Authorization\", payment.authorization);\n const retry = await globalThis.fetch(url, { ...init, headers: retryHeaders });\n\n if (retry.status === 402) {\n throw new L402PaymentFailedError(\n \"Server returned 402 after successful payment\",\n );\n }\n\n return retry;\n }\n\n async function jsonMethod(method: string, url: string, init?: RequestInit) {\n const res = await l402Fetch(url, { ...init, method });\n return res.json();\n }\n\n return {\n fetch: l402Fetch,\n get: (url: string, init?: RequestInit) => jsonMethod(\"GET\", url, init),\n post: (url: string, init?: RequestInit) => jsonMethod(\"POST\", url, init),\n put: (url: string, init?: RequestInit) => jsonMethod(\"PUT\", url, init),\n patch: (url: string, init?: RequestInit) => jsonMethod(\"PATCH\", url, init),\n delete: (url: string, init?: RequestInit) => jsonMethod(\"DELETE\", url, init),\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lnbot/l402",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "L402 Lightning payment middleware for Express.js — paywall any API in one line",
5
5
  "type": "module",
6
6
  "exports": {