@getalby/lightning-tools
Version:
Collection of helpful building blocks and tools to develop Bitcoin Lightning web apps
158 lines (152 loc) • 5.81 kB
JavaScript
;
/**
* 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