From 12c79de0bec0b1c7a8f7443db698ff38985f8737 Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:36:23 +0200 Subject: [PATCH 1/4] fix(lightning): add X-Forwarded-Proto header to LNBits API calls (#3890) * fix(lightning): add X-Forwarded-Proto header to LNBits API calls LNBits requires HTTPS for LNURL endpoints and returns 426 when called over plain HTTP. On Azure, nginx added this header automatically. After migration to dfxdev, dfx-api calls LNBits directly without the header. * fix(lightning): add X-Forwarded-Proto header to LNBits API calls LNBits requires HTTPS for LNURL endpoints and returns 426 when called over plain HTTP. On Azure, nginx added this header automatically. After migration to dfxdev, dfx-api calls LNBits directly without the header. --- src/integration/lightning/lightning-client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/integration/lightning/lightning-client.ts b/src/integration/lightning/lightning-client.ts index 570132153d..1f75efcef2 100644 --- a/src/integration/lightning/lightning-client.ts +++ b/src/integration/lightning/lightning-client.ts @@ -366,6 +366,7 @@ export class LightningClient implements CoinOnly { private httpLnBitsConfig(params?: any): HttpRequestConfig { return { httpsAgent: this.tlsAgent, + headers: { 'X-Forwarded-Proto': 'https' }, params: { 'api-key': Config.blockchain.lightning.lnbits.apiKey, ...params }, }; } From 7446756ddf83fac3c17b295c1ba88d407fc90046 Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:01:29 +0200 Subject: [PATCH 2/4] refactor(lightning): move LNURL id validation from LightningClient to LnUrlForwardService (#3894) Validation belongs at the service layer (HTTP input boundary), not in the data-access layer. LightningClient is also used from non-HTTP contexts where BadRequestException is inappropriate. --- src/integration/lightning/lightning-client.ts | 20 ------------------- .../services/lnurl-forward.service.ts | 18 +++++++++++++++++ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/integration/lightning/lightning-client.ts b/src/integration/lightning/lightning-client.ts index 1f75efcef2..cf6ec6ea6e 100644 --- a/src/integration/lightning/lightning-client.ts +++ b/src/integration/lightning/lightning-client.ts @@ -1,4 +1,3 @@ -import { BadRequestException } from '@nestjs/common'; import { randomBytes } from 'crypto'; import { Agent } from 'https'; import { Config } from 'src/config/config'; @@ -200,15 +199,11 @@ export class LightningClient implements CoinOnly { // --- LNURLp REWRITE --- // async getLnurlpPaymentRequest(linkId: string): Promise { - this.validateLinkId(linkId); - const lnBitsUrl = `${Config.blockchain.lightning.lnbits.lnurlpUrl}/${linkId}`; return this.http.get(lnBitsUrl, this.httpLnBitsConfig()); } async getLnurlpInvoice(linkId: string, params: any): Promise { - this.validateLinkId(linkId); - const lnBitsCallbackUrl = `${Config.blockchain.lightning.lnbits.lnurlpApiUrl}/lnurl/cb/${linkId}`; return this.http.get(lnBitsCallbackUrl, this.httpLnBitsConfig(params)); } @@ -222,8 +217,6 @@ export class LightningClient implements CoinOnly { } async getLnurlpLink(linkId: string): Promise { - this.validateLinkId(linkId); - return this.http.get( `${Config.blockchain.lightning.lnbits.lnurlpApiUrl}/links/${linkId}`, this.httpLnBitsConfig(), @@ -254,8 +247,6 @@ export class LightningClient implements CoinOnly { } async updateLnurlpLink(linkId: string, data: LnurlpLinkUpdateDto): Promise { - this.validateLinkId(linkId); - return this.http.put( `${Config.blockchain.lightning.lnbits.lnurlpApiUrl}/links/${linkId}`, data, @@ -298,8 +289,6 @@ export class LightningClient implements CoinOnly { } async getLnurlwLink(linkId: string): Promise { - this.validateLinkId(linkId); - return this.http.get( `${Config.blockchain.lightning.lnbits.lnurlwApiUrl}/links/${linkId}`, this.httpLnBitsConfig(), @@ -339,16 +328,11 @@ export class LightningClient implements CoinOnly { // --- LNURLd --- // async getLnurlDevice(id: string, params: any): Promise { - this.validateLinkId(id); - const url = `${this.getDeviceUrl()}/${id}`; return this.http.get(url, this.httpLnBitsConfig(params)); } async getLnurlDeviceCallback(id: string, variable: string, params: any): Promise { - this.validateLinkId(id); - this.validateLinkId(variable); - const url = `${this.getDeviceUrl()}/cb/${id}/${variable}`; return this.http.get(url, this.httpLnBitsConfig(params)); } @@ -359,10 +343,6 @@ export class LightningClient implements CoinOnly { } // --- HELPER METHODS --- // - private validateLinkId(linkId: string): void { - if (!/^[\w-]+$/.test(linkId)) throw new BadRequestException('Invalid link id'); - } - private httpLnBitsConfig(params?: any): HttpRequestConfig { return { httpsAgent: this.tlsAgent, diff --git a/src/subdomains/generic/forwarding/services/lnurl-forward.service.ts b/src/subdomains/generic/forwarding/services/lnurl-forward.service.ts index 42102a90c7..1f72fdaef0 100644 --- a/src/subdomains/generic/forwarding/services/lnurl-forward.service.ts +++ b/src/subdomains/generic/forwarding/services/lnurl-forward.service.ts @@ -49,6 +49,8 @@ export class LnUrlForwardService { id: string, params: any, ): Promise { + this.validateLinkId(id); + if (id.startsWith(this.PAYMENT_LINK_PREFIX) || id.startsWith(this.PAYMENT_LINK_PAYMENT_PREFIX)) { const payRequest = await this.paymentLinkService.createPayRequest( id, @@ -78,6 +80,8 @@ export class LnUrlForwardService { // callback async lnurlpCallbackForward(id: string, params: any): Promise { + this.validateLinkId(id); + if (id.startsWith(this.PAYMENT_LINK_PREFIX) || id.startsWith(this.PAYMENT_LINK_PAYMENT_PREFIX)) { const transferInfo = this.getPaymentTransferInfo(params); return this.paymentLinkPaymentService.createActivationRequest(id, transferInfo); @@ -138,6 +142,8 @@ export class LnUrlForwardService { // --- LNURLw --- // async lnurlwForward(id: string): Promise { + this.validateLinkId(id); + const withdrawRequest = await this.client.getLnurlwWithdrawRequest(id); withdrawRequest.callback = LightningHelper.createLnurlwCallbackUrl(id); @@ -146,11 +152,15 @@ export class LnUrlForwardService { } async lnurlwCallbackForward(id: string, params: any): Promise { + this.validateLinkId(id); + return this.client.sendLnurlwInvoice(id, params); } // --- LNURLd --- // async lnurldForward(deviceId: string, params: any): Promise { + this.validateLinkId(deviceId); + const withdrawRequest = await this.client.getLnurlDevice(deviceId, params); const [paymentId, variable] = withdrawRequest.callback.split('/').slice(-2); @@ -160,6 +170,9 @@ export class LnUrlForwardService { } async lnurldCallbackForward(id: string, variable: string, params: any): Promise { + this.validateLinkId(id); + this.validateLinkId(variable); + return this.client.getLnurlDeviceCallback(id, variable, params); } @@ -172,4 +185,9 @@ export class LnUrlForwardService { return this.paymentLinkService.createPayRequest(pendingPayment.uniqueId); } + + // --- HELPERS --- // + private validateLinkId(id: string): void { + if (!/^[\w-]+$/.test(id)) throw new BadRequestException('Invalid link id'); + } } From 2b0005923ae968c942b3ecedcd3ecf05c83e6006 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:02:32 +0200 Subject: [PATCH 3/4] fix(log): mark high totalBalanceChf FinancialDataLog entries of 2026-06-16 as invalid (#3892) --- ...-InvalidateHighTotalBalanceLogs20260616.js | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 migration/1781598468039-InvalidateHighTotalBalanceLogs20260616.js diff --git a/migration/1781598468039-InvalidateHighTotalBalanceLogs20260616.js b/migration/1781598468039-InvalidateHighTotalBalanceLogs20260616.js new file mode 100644 index 0000000000..1cf0a85e0a --- /dev/null +++ b/migration/1781598468039-InvalidateHighTotalBalanceLogs20260616.js @@ -0,0 +1,63 @@ +// FinancialDataLog entries created on 2026-06-16 carry a totalBalanceChf that is +// implausibly high (> 50 000 CHF) compared to the expected operating-equity range, +// yet they were stamped valid=true. This is a recurrence of the same transient +// accounting spike handled for 2026-06-15 (see InvalidateHighTotalBalanceLogs +// 1781527084203): plusBalanceChf jumped ahead of the corresponding minusBalanceChf +// booking, so deltas at the elevated baseline stayed within +// financeLogTotalBalanceChangeLimit and the invalid flag was never set. This +// migration marks those entries invalid so monitoring dashboards and anomaly +// alerts reflect the period correctly. +// +// Threshold : totalBalanceChf > 50 000 (well above the normal ~20–30 k band) +// Scope : entries created on 2026-06-16 (UTC), no upper bound so every +// affected row of the day is covered regardless of sub-second timing. +// +// Env-guarded: the COUNT pre-check makes up() a no-op where no rows match +// (staging/dev). down() re-stamps valid=true for the same window/threshold; it +// cannot distinguish rows already invalid before up() ran, so it may over-restore +// a small number of entries — accepted for this one-shot fix. +module.exports = class InvalidateHighTotalBalanceLogs1781598468039 { + name = 'InvalidateHighTotalBalanceLogs1781598468039'; + + async up(queryRunner) { + const [{ count }] = await queryRunner.query(` + SELECT COUNT(*) AS count FROM log + WHERE subsystem = 'FinancialDataLog' + AND created >= '2026-06-16T00:00:00Z' + AND created < '2026-06-17T00:00:00Z' + AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000 + AND valid = true + `); + if (parseInt(count) === 0) return; + + await queryRunner.query(` + UPDATE log SET valid = false + WHERE subsystem = 'FinancialDataLog' + AND created >= '2026-06-16T00:00:00Z' + AND created < '2026-06-17T00:00:00Z' + AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000 + AND valid = true + `); + } + + async down(queryRunner) { + const [{ count }] = await queryRunner.query(` + SELECT COUNT(*) AS count FROM log + WHERE subsystem = 'FinancialDataLog' + AND created >= '2026-06-16T00:00:00Z' + AND created < '2026-06-17T00:00:00Z' + AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000 + AND valid = false + `); + if (parseInt(count) === 0) return; + + await queryRunner.query(` + UPDATE log SET valid = true + WHERE subsystem = 'FinancialDataLog' + AND created >= '2026-06-16T00:00:00Z' + AND created < '2026-06-17T00:00:00Z' + AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000 + AND valid = false + `); + } +}; From 3d8593698872253915e2c88be746adc0b9547d5b Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:21:40 +0200 Subject: [PATCH 4/4] fix(lightning): forward public Host header to LNBits for LNURL URL validation (#3893) --- src/integration/lightning/lightning-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integration/lightning/lightning-client.ts b/src/integration/lightning/lightning-client.ts index cf6ec6ea6e..fbed415cc4 100644 --- a/src/integration/lightning/lightning-client.ts +++ b/src/integration/lightning/lightning-client.ts @@ -346,7 +346,7 @@ export class LightningClient implements CoinOnly { private httpLnBitsConfig(params?: any): HttpRequestConfig { return { httpsAgent: this.tlsAgent, - headers: { 'X-Forwarded-Proto': 'https' }, + headers: { 'X-Forwarded-Proto': 'https', Host: new URL(Config.url()).hostname }, params: { 'api-key': Config.blockchain.lightning.lnbits.apiKey, ...params }, }; }