@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.
- package/dist/client/index.cjs +18 -11
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +18 -11
- package/dist/client/index.js.map +1 -1
- package/dist/{fetch-BPQpEg8M.d.ts → fetch-B2UA6hNH.d.ts} +6 -0
- package/dist/{fetch-B0tuycqO.d.cts → fetch-BbNwLZKo.d.cts} +6 -0
- package/dist/index.cjs +18 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/client/index.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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"]}
|
package/dist/client/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { L as L402Client, c as client } from '../fetch-
|
|
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';
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { L as L402Client, c as client } from '../fetch-
|
|
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';
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 {
|
package/dist/client/index.js.map
CHANGED
|
@@ -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":";AAAO,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,0BAAN,cAAsC,UAAU;AAAA,EACrD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,yBAAN,cAAqC,UAAU;AAAA,EACpD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;;AClBO,SAAS,mBACd,QAC+C;AAC/C,MAAI,CAAC,OAAO,WAAW,OAAO,EAAG,QAAO;AACxC,QAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,QAAM,aAAa,MAAM,YAAY,GAAG;AACxC,MAAI,eAAe,GAAI,QAAO;AAC9B,SAAO;AAAA,IACL,UAAU,MAAM,MAAM,GAAG,UAAU;AAAA,IACnC,UAAU,MAAM,MAAM,aAAa,CAAC;AAAA,EACtC;AACF;AAGO,SAAS,eACd,QAC8C;AAC9C,MAAI,CAAC,OAAO,WAAW,OAAO,EAAG,QAAO;AACxC,QAAM,gBAAgB,OAAO,MAAM,oBAAoB;AACvD,QAAM,eAAe,OAAO,MAAM,mBAAmB;AACrD,MAAI,CAAC,iBAAiB,CAAC,aAAc,QAAO;AAC5C,SAAO,EAAE,UAAU,cAAc,CAAC,GAAG,SAAS,aAAa,CAAC,EAAE;AAChE;;;ACpBA,IAAM,YAAoC;AAAA,EACxC,MAAM,KAAK,KAAK;AAAA,EAChB,KAAK,KAAK,KAAK,KAAK;AAAA,EACpB,MAAM,IAAI,KAAK,KAAK,KAAK;AAAA,EACzB,OAAO,KAAK,KAAK,KAAK,KAAK;AAC7B;AAGO,IAAM,SAAN,MAAa;AAAA,EACV,QAAQ;AAAA,EACR,cAAc,KAAK,IAAI;AAAA,EACd;AAAA,EACA;AAAA,EAEjB,YAAY,SAA4B;AACtC,SAAK,YAAY,QAAQ;AACzB,QAAI,QAAQ,cAAc;AACxB,WAAK,WAAW,UAAU,QAAQ,YAAY;AAAA,IAChD;AAAA,EACF;AAAA,EAEQ,aAAmB;AACzB,QAAI,KAAK,YAAY,KAAK,IAAI,IAAI,KAAK,eAAe,KAAK,UAAU;AACnE,WAAK,QAAQ;AACb,WAAK,cAAc,KAAK,IAAI;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,OAAqB;AACzB,QAAI,KAAK,cAAc,OAAW;AAClC,SAAK,WAAW;AAChB,QAAI,KAAK,QAAQ,QAAQ,KAAK,WAAW;AACvC,YAAM,IAAI;AAAA,QACR,cAAc,KAAK,8BAA8B,KAAK,KAAK,IAAI,KAAK,SAAS;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,OAAqB;AAC1B,SAAK,WAAW;AAChB,SAAK,SAAS;AAAA,EAChB;AACF;;;AC5CA,SAAS,aAAa,KAAqB;AACzC,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,MAAE,SAAS;AACX,MAAE,OAAO;AACT,WAAO,EAAE,SAAS,EAAE,QAAQ,QAAQ,EAAE;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,IAAM,cAAN,MAAwC;AAAA,EACrC,SAAS,oBAAI,IAAuB;AAAA,EAE5C,MAAM,IAAI,KAAwC;AAChD,WAAO,KAAK,OAAO,IAAI,aAAa,GAAG,CAAC,KAAK;AAAA,EAC/C;AAAA,EAEA,MAAM,IAAI,KAAa,OAAiC;AACtD,SAAK,OAAO,IAAI,aAAa,GAAG,GAAG,KAAK;AAAA,EAC1C;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,SAAK,OAAO,OAAO,aAAa,GAAG,CAAC;AAAA,EACtC;AACF;AAGO,IAAM,UAAN,MAAoC;AAAA,EACzC,MAAM,MAAqB;AACzB,WAAO;AAAA,EACT;AAAA,EACA,MAAM,MAAqB;AAAA,EAAC;AAAA,EAC5B,MAAM,SAAwB;AAAA,EAAC;AACjC;AAGO,SAAS,aACd,OACY;AACZ,MAAI,CAAC,SAAS,UAAU,SAAU,QAAO,IAAI,YAAY;AACzD,MAAI,UAAU,OAAQ,QAAO,IAAI,QAAQ;AACzC,SAAO;AACT;;;ACtBO,SAAS,OAAO,IAAW,UAA6B,CAAC,GAAe;AAC7E,QAAM,QAAQ,aAAa,QAAQ,KAAK;AACxC,QAAM,SAAS,IAAI,OAAO,OAAO;AACjC,QAAM,WAAW,QAAQ,YAAY;AAErC,iBAAe,UACb,KACA,MACmB;AAEnB,UAAM,SAAS,MAAM,MAAM,IAAI,GAAG;AAClC,QAAI,QAAQ;AACV,YAAM,YACJ,OAAO,aAAa,OAAO,UAAU,QAAQ,KAAK,KAAK,IAAI;AAC7D,UAAI,CAAC,WAAW;AACd,cAAM,UAAU,IAAI,QAAQ,MAAM,OAAO;AACzC,gBAAQ,IAAI,iBAAiB,OAAO,aAAa;AACjD,cAAMA,OAAM,MAAM,WAAW,MAAM,KAAK,EAAE,GAAG,MAAM,QAAQ,CAAC;AAE5D,YAAIA,KAAI,WAAW,IAAK,QAAOA;AAC/B,cAAM,MAAM,OAAO,GAAG;AAAA,MACxB,OAAO;AACL,cAAM,MAAM,OAAO,GAAG;AAAA,MACxB;AAAA,IACF;AAGA,UAAM,MAAM,MAAM,WAAW,MAAM,KAAK,IAAI;AAC5C,QAAI,IAAI,WAAW,IAAK,QAAO;AAG/B,UAAM,UAAU,IAAI,QAAQ,IAAI,kBAAkB;AAClD,QAAI,CAAC;AACH,YAAM,IAAI,UAAU,8CAA8C;AAEpE,UAAM,YAAY,eAAe,OAAO;AACxC,QAAI,CAAC,UAAW,OAAM,IAAI,UAAU,gCAAgC;AAGpE,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAC9C,UAAM,QAAgB,MAAM,SAAS;AAGrC,QAAI,QAAQ,UAAU;AACpB,YAAM,IAAI;AAAA,QACR,SAAS,KAAK,0BAA0B,QAAQ;AAAA,MAClD;AAAA,IACF;AACA,WAAO,MAAM,KAAK;AAGlB,UAAM,UAAU,MAAM,GAAG,KAAK,IAAI,EAAE,iBAAiB,QAAQ,CAAC;AAE9D,QAAI,QAAQ,WAAW,UAAU;AAC/B,YAAM,IAAI,uBAAuB,qBAAqB;AAAA,IACxD;AACA,QAAI,CAAC,QAAQ,eAAe;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,UAAM,SAAS,mBAAmB,QAAQ,aAAa;AACvD,UAAM,MAAM,IAAI,KAAK;AAAA,MACnB,UAAU,QAAQ,YAAY,UAAU;AAAA,MACxC,UAAU,QAAQ,YAAY,QAAQ,YAAY;AAAA,MAClD,eAAe,QAAQ;AAAA,MACvB,QAAQ,oBAAI,KAAK;AAAA,MACjB,WAAW,MAAM,YACb,IAAI,KAAK,KAAK,SAAS,IACvB;AAAA,IACN,CAAC;AAED,WAAO,OAAO,KAAK;AAGnB,UAAM,eAAe,IAAI,QAAQ,MAAM,OAAO;AAC9C,iBAAa,IAAI,iBAAiB,QAAQ,aAAa;AACvD,WAAO,WAAW,MAAM,KAAK,EAAE,GAAG,MAAM,SAAS,aAAa,CAAC;AAAA,EACjE;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM,IAAI,KAAa,MAAoB;AACzC,YAAM,MAAM,MAAM,UAAU,KAAK,EAAE,GAAG,MAAM,QAAQ,MAAM,CAAC;AAC3D,aAAO,IAAI,KAAK;AAAA,IAClB;AAAA,IACA,MAAM,KAAK,KAAa,MAAoB;AAC1C,YAAM,MAAM,MAAM,UAAU,KAAK,EAAE,GAAG,MAAM,QAAQ,OAAO,CAAC;AAC5D,aAAO,IAAI,KAAK;AAAA,IAClB;AAAA,EACF;AACF;","names":["res"]}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -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-
|
|
3
|
-
export { L as L402Client } from './fetch-
|
|
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-
|
|
3
|
-
export { L as L402Client } from './fetch-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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"]}
|