• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cameri / nostream / 24613525839

18 Apr 2026 08:44PM UTC coverage: 13.774% (-54.5%) from 68.308%
24613525839

Pull #476

github

web-flow
Merge 2acb6291f into e1a7bfb16
Pull Request #476: feat: implement EWMA rate limiter with strategy support (#404)

7 of 1479 branches covered (0.47%)

Branch coverage included in aggregate %.

13 of 47 new or added lines in 10 files covered. (27.66%)

1746 existing lines in 65 files now uncovered.

673 of 3458 relevant lines covered (19.46%)

0.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

10.47
/src/controllers/callbacks/lnbits-callback-controller.ts
1
import { Request, Response } from 'express'
2

3
import { deriveFromSecret, hmacSha256 } from '../../utils/secret'
1✔
4
import { Invoice, InvoiceStatus } from '../../@types/invoice'
1✔
5
import { lnbitsCallbackBodySchema, lnbitsCallbackQuerySchema } from '../../schemas/lnbits-callback-schema'
1✔
6
import { createLogger } from '../../factories/logger-factory'
1✔
7
import { createSettings } from '../../factories/settings-factory'
1✔
8
import { getRemoteAddress } from '../../utils/http'
1✔
9
import { IController } from '../../@types/controllers'
10
import { IInvoiceRepository } from '../../@types/repositories'
11
import { IPaymentsService } from '../../@types/services'
12
import { validateSchema } from '../../utils/validation'
1✔
13

14
const debug = createLogger('lnbits-callback-controller')
1✔
15

16
export class LNbitsCallbackController implements IController {
1✔
17
  public constructor(
UNCOV
18
    private readonly paymentsService: IPaymentsService,
×
UNCOV
19
    private readonly invoiceRepository: IInvoiceRepository,
×
20
  ) {}
21

22
  public async handleRequest(request: Request, response: Response) {
UNCOV
23
    debug('request headers: %o', request.headers)
×
UNCOV
24
    debug('request body: %o', request.body)
×
25

UNCOV
26
    const settings = createSettings()
×
UNCOV
27
    const remoteAddress = getRemoteAddress(request, settings)
×
UNCOV
28
    const paymentProcessor = settings.payments?.processor ?? 'null'
×
29

UNCOV
30
    if (paymentProcessor !== 'lnbits') {
×
UNCOV
31
      debug('denied request from %s to /callbacks/lnbits which is not the current payment processor', remoteAddress)
×
UNCOV
32
      response.status(403).send('Forbidden')
×
UNCOV
33
      return
×
34
    }
35

UNCOV
36
    const queryValidation = validateSchema(lnbitsCallbackQuerySchema)(request.query)
×
UNCOV
37
    if (queryValidation.error) {
×
UNCOV
38
      debug('unauthorized request from %s to /callbacks/lnbits: invalid query %o', remoteAddress, queryValidation.error)
×
UNCOV
39
      response.status(403).send('Forbidden')
×
UNCOV
40
      return
×
41
    }
42

UNCOV
43
    const hmac = request.query.hmac as string
×
UNCOV
44
    const split = hmac.split(':')
×
UNCOV
45
    const expiryString = split[0]
×
UNCOV
46
    const expiry = Number(expiryString)
×
UNCOV
47
    const hasValidSplit = split.length === 2
×
UNCOV
48
    const hasValidExpiry = /^\d+$/.test(expiryString) && Number.isSafeInteger(expiry)
×
UNCOV
49
    if (
×
50
      !hasValidSplit ||
×
51
      hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), expiryString).toString('hex') !== split[1] ||
52
      !hasValidExpiry ||
53
      expiry <= Date.now()
54
    ) {
UNCOV
55
      debug('unauthorized request from %s to /callbacks/lnbits: hmac signature mismatch or expired', remoteAddress)
×
UNCOV
56
      response.status(403).send('Forbidden')
×
UNCOV
57
      return
×
58
    }
59

UNCOV
60
    const bodyValidation = validateSchema(lnbitsCallbackBodySchema)(request.body)
×
UNCOV
61
    if (bodyValidation.error) {
×
UNCOV
62
      response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Malformed body')
×
UNCOV
63
      return
×
64
    }
65

UNCOV
66
    const body = request.body
×
UNCOV
67
    const invoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(body.payment_hash)
×
UNCOV
68
    const storedInvoice = await this.invoiceRepository.findById(body.payment_hash)
×
69

UNCOV
70
    if (!storedInvoice) {
×
UNCOV
71
      response.status(404).setHeader('content-type', 'text/plain; charset=utf8').send('No such invoice')
×
UNCOV
72
      return
×
73
    }
74

UNCOV
75
    try {
×
UNCOV
76
      await this.paymentsService.updateInvoice(invoice)
×
77
    } catch (error) {
UNCOV
78
      console.error(`Unable to persist invoice ${invoice.id}`, error)
×
79

UNCOV
80
      throw error
×
81
    }
82

UNCOV
83
    if (invoice.status !== InvoiceStatus.COMPLETED && !invoice.confirmedAt) {
×
UNCOV
84
      response.status(200).send()
×
85

UNCOV
86
      return
×
87
    }
88

UNCOV
89
    if (storedInvoice.status === InvoiceStatus.COMPLETED) {
×
UNCOV
90
      response.status(409).setHeader('content-type', 'text/plain; charset=utf8').send('Invoice is already marked paid')
×
UNCOV
91
      return
×
92
    }
93

UNCOV
94
    invoice.amountPaid = invoice.amountRequested
×
95

UNCOV
96
    try {
×
UNCOV
97
      await this.paymentsService.confirmInvoice(invoice as Invoice)
×
UNCOV
98
      await this.paymentsService.sendInvoiceUpdateNotification(invoice as Invoice)
×
99
    } catch (error) {
UNCOV
100
      console.error(`Unable to confirm invoice ${invoice.id}`, error)
×
101

UNCOV
102
      throw error
×
103
    }
104

UNCOV
105
    response.status(200).setHeader('content-type', 'text/plain; charset=utf8').send('OK')
×
106
  }
107
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc