Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/lib/lnurl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { bech32, utf8 } from '@scure/base'

const corsProxyUrl = 'https://cors-header-proxy.bordalix.workers.dev/proxy?apiurl='
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Hardcoded personal CORS proxy is a critical trust, privacy, and reliability blocker.

All LNURL-related network traffic — including payment callbacks, Lightning invoices, and Ark addresses — is routed through cors-header-proxy.bordalix.workers.dev, a proxy on an individual developer's personal account. This carries several hard blockers for any merge:

  1. Trust/Privacy: Users' payment amounts, invoice data, and receiving addresses are all readable by the proxy operator.
  2. Availability: The Workers Free plan has a daily request limit of 100,000 requests, and when this limit is exceeded, Cloudflare returns Error 1027 — causing silent failures for all wallet users after the cap is hit.
  3. Single point of failure: If the worker is deleted, redeployed, or misconfigured, the entire LNURL feature breaks for all users with no fallback.

Before this can be merged, the solution needs either:

  • A properly governed, project-owned proxy (or a server-side relay endpoint in the wallet's own backend), or
  • A CORS-native approach (e.g., configuring LNURL servers/LN addresses you control to emit correct headers, or using a user-configurable proxy setting).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/lnurl.ts` at line 3, The constant corsProxyUrl (in lnurl.ts) must not
point to a hardcoded personal proxy; replace this by reading a configurable
value (e.g., an environment/runtime config or injected setting) and avoid
silently forcing all LNURL traffic through it—if no project-owned proxy is
configured, fall back to direct requests or fail with a clear error; ensure all
call sites that reference corsProxyUrl use the new config accessor and add
validation that the configured proxy is organization-owned (or empty) and
documented so operators can supply their own proxy or backend relay.


const emailRegex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

Expand All @@ -26,6 +28,13 @@ type LnUrlCallbackResponse = {
pr: string
}

const fetchWithCorsProxy = (url: string, options?: RequestInit): Promise<Response> => {
// don't use proxy in tests to avoid CORS issues with Playwright's request interception
const isPlaywrightTest = typeof process !== 'undefined' && process?.env?.PLAYWRIGHT_TEST === '1'
const proxyUrl = isPlaywrightTest ? url : `${corsProxyUrl}${encodeURIComponent(url)}`
return fetch(proxyUrl, options)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const checkResponse = <T = any>(response: Response): Promise<T> => {
if (!response.ok) return Promise.reject(response)
return response.json()
Expand All @@ -41,7 +50,7 @@ const checkLnUrlResponse = (amount: number, data: LnUrlResponse) => {
const fetchLnUrlInvoice = async (amount: number, note: string, data: LnUrlResponse) => {
let url = `${data.callback}?amount=${amount}`
if (note) url += `&comment=${note}`
const res = await fetch(url).then(checkResponse<LnUrlCallbackResponse>)
const res = await fetchWithCorsProxy(url).then(checkResponse<LnUrlCallbackResponse>)
Comment on lines 52 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

note is still appended unencoded, breaking the comment value at the LNURL server.

encodeURIComponent(url) in fetchWithCorsProxy correctly protects the proxy query-string structure (previous review ✅). However, the LNURL server decodes the forwarded URL and still sees comment=test&evil=1 as two separate query parameters when note contains &. The comment value received by the server is silently truncated.

🐛 Proposed fix
-  if (note) url += `&comment=${note}`
+  if (note) url += `&comment=${encodeURIComponent(note)}`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/lnurl.ts` around lines 50 - 51, The comment value is appended
unencoded to the LNURL callback URL (variable note) which allows characters like
& to split into extra query params; update the code that builds url (where note
is appended) to encode the comment value with encodeURIComponent (e.g., change
url += `&comment=${note}` to url += `&comment=${encodeURIComponent(note)}`) so
the LNURL server receives the full comment string; this interacts safely with
the existing fetchWithCorsProxy call and checkResponse<LnUrlCallbackResponse>.

return res.pr
}

Expand Down Expand Up @@ -78,7 +87,7 @@ export const getCallbackUrl = (lnurl: string): string => {
export const checkLnUrlConditions = (lnurl: string): Promise<LnUrlResponse> => {
return new Promise<LnUrlResponse>((resolve, reject) => {
const url = getCallbackUrl(lnurl)
fetch(url)
fetchWithCorsProxy(url)
.then(checkResponse<LnUrlResponse>)
.then(resolve)
.catch(reject)
Expand All @@ -89,7 +98,7 @@ export const fetchInvoice = (lnurl: string, sats: number, note: string): Promise
return new Promise<string>((resolve, reject) => {
const url = getCallbackUrl(lnurl)
const amount = Math.round(sats * 1000) // millisatoshis
fetch(url)
fetchWithCorsProxy(url)
.then(checkResponse<LnUrlResponse>)
.then((data) => checkLnUrlResponse(amount, data))
.then((data) => fetchLnUrlInvoice(amount, note, data))
Expand All @@ -101,7 +110,7 @@ export const fetchInvoice = (lnurl: string, sats: number, note: string): Promise
export const fetchArkAddress = (lnurl: string): Promise<ArkMethodResponse> => {
return new Promise<ArkMethodResponse>((resolve, reject) => {
const url = getCallbackUrl(lnurl) + '?method=ark'
fetch(url)
fetchWithCorsProxy(url)
.then(checkResponse<ArkMethodResponse>)
.then(resolve)
.catch(reject)
Expand Down
Loading