UNPKG

@getalby/lightning-tools

Version:

Collection of helpful building blocks and tools to develop Bitcoin Lightning web apps

158 lines (152 loc) 5.81 kB
'use strict'; /** * Client: parse "www-authenticate" header from server response * @param input * @returns details from the header value (token or macaroon, invoice) */ const parseL402 = (input) => { // Remove the L402 and LSAT identifiers const string = input.replace("L402", "").replace("LSAT", "").trim(); // Initialize an object to store the key-value pairs const keyValuePairs = {}; // Regular expression to match key and (quoted or unquoted) value const regex = /(\w+)=("([^"]*)"|'([^']*)'|([^,]*))/g; let match; // Use regex to find all key-value pairs while ((match = regex.exec(string)) !== null) { // Key is always match[1] // Value is either match[3] (double-quoted), match[4] (single-quoted), or match[5] (unquoted) keyValuePairs[match[1]] = match[3] || match[4] || match[5]; } if (!keyValuePairs["token"] && keyValuePairs["macaroon"]) { // fallback to old naming keyValuePairs["token"] = keyValuePairs["macaroon"]; delete keyValuePairs["macaroon"]; } if (!("token" in keyValuePairs) || typeof keyValuePairs["token"] !== "string") { throw new Error("No macaroon or token found in www-authenticate header"); } if (!("invoice" in keyValuePairs) || typeof keyValuePairs["invoice"] !== "string") { throw new Error("No invoice found in www-authenticate header"); } return keyValuePairs; }; const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) => { const details = parseL402(l402Header); const token = details.token || details.macaroon; const invoice = details.invoice; if (!token) { throw new Error("L402: missing token/macaroon in WWW-Authenticate header"); } if (!invoice) { throw new Error("L402: missing invoice in WWW-Authenticate header"); } const invResp = await wallet.payInvoice({ invoice }); headers.set("Authorization", `L402 ${token}:${invResp.preimage}`); return fetch(url, fetchArgs); }; const fetchWithL402 = async (url, fetchArgs, options) => { const wallet = options.wallet; if (!wallet) { throw new Error("wallet is missing"); } if (!fetchArgs) { fetchArgs = {}; } fetchArgs.cache = "no-store"; fetchArgs.mode = "cors"; const headers = new Headers(fetchArgs.headers ?? undefined); fetchArgs.headers = headers; const initResp = await fetch(url, fetchArgs); const header = initResp.headers.get("www-authenticate"); if (!header) { return initResp; } return handleL402Payment(header, url, fetchArgs, headers, wallet); }; async function issueL402Macaroon(secret, paymentHash, params) { if (params !== undefined && Object.prototype.hasOwnProperty.call(params, "paymentHash")) { throw new Error("paymentHash is reserved"); } const payload = { ...params, paymentHash }; const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url"); const mac = await sign(secret, encoded); return `${encoded}.${mac}`; } async function verifyL402Macaroon(secret, token) { const { timingSafeEqual } = await import('crypto'); const dotIndex = token.lastIndexOf("."); if (dotIndex === -1) throw new Error("Invalid macaroon token"); const encoded = token.slice(0, dotIndex); const mac = token.slice(dotIndex + 1); // Constant-time comparison to prevent timing attacks const expectedMac = await sign(secret, encoded); try { if (!timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(expectedMac, "hex"))) { throw new Error("Invalid macaroon token"); } } catch (e) { throw new Error("Invalid macaroon token"); } try { const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.paymentHash !== "string") { throw new Error("Invalid macaroon payload"); } return parsed; } catch { throw new Error("Invalid macaroon token"); } } async function sign(secret, payload) { const { createHmac } = await import('crypto'); return createHmac("sha256", secret).update(payload).digest("hex"); } /** * Server: create a WWW-Authenticate header for a given macaroon and invoice * @param args the macaroon/token and invoice generated for the client's request * @returns the header value */ const makeL402AuthenticateHeader = (args) => { if (!args.token) { throw new Error("token must be provided"); } return `L402 version="0" token="${args.token}", invoice="${args.invoice}"`; }; /** * Server: parse "authorization" header sent from client * @param input value from authorization header * @returns the macaroon and preimage */ function parseL402Authorization(input) { // Backwards compat: LSAT was the former name of L402 const normalized = input.replace(/^LSAT /, "L402 "); const prefix = "L402 "; if (!normalized.startsWith(prefix)) return null; const credentials = normalized.slice(prefix.length); const colonIndex = credentials.indexOf(":"); if (colonIndex === -1) { throw new Error("Invalid authorization header value"); } return { token: credentials.slice(0, colonIndex), preimage: credentials.slice(colonIndex + 1), }; } exports.fetchWithL402 = fetchWithL402; exports.issueL402Macaroon = issueL402Macaroon; exports.makeL402AuthenticateHeader = makeL402AuthenticateHeader; exports.parseL402 = parseL402; exports.parseL402Authorization = parseL402Authorization; exports.verifyL402Macaroon = verifyL402Macaroon; //# sourceMappingURL=l402.cjs.map