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

cameri / nostream / 26300017967

22 May 2026 04:37PM UTC coverage: 36.478% (-28.6%) from 65.107%
26300017967

Pull #622

github

web-flow
Merge cbfcf131b into 36e5af87e
Pull Request #622: feat: add NIP-42 client authentication

791 of 3191 branches covered (24.79%)

Branch coverage included in aggregate %.

21 of 63 new or added lines in 6 files covered. (33.33%)

1585 existing lines in 77 files now uncovered.

2608 of 6127 relevant lines covered (42.57%)

17.63 hits per line

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

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

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

14
const logger = createLogger('lnbits-callback-controller')
2✔
15

16
export class LNbitsCallbackController implements IController {
2✔
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
    logger('request headers: %o', request.headers)
×
UNCOV
24
    logger('request body: %o', request.body)
×
25

UNCOV
26
    const settings = createSettings()
×
UNCOV
27
    const remoteAddress = getRemoteAddress(request, settings)
×
28

UNCOV
29
    const queryValidation = validateSchema(lnbitsCallbackQuerySchema)(request.query)
×
UNCOV
30
    if (queryValidation.error) {
×
UNCOV
31
      logger('unauthorized request from %s to /callbacks/lnbits: invalid query %o', remoteAddress, queryValidation.error)
×
UNCOV
32
      response.status(403).send('Forbidden')
×
UNCOV
33
      return
×
34
    }
35

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

UNCOV
53
    const bodyValidation = validateSchema(lnbitsCallbackBodySchema)(request.body)
×
UNCOV
54
    if (bodyValidation.error) {
×
UNCOV
55
      response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Malformed body')
×
UNCOV
56
      return
×
57
    }
58

UNCOV
59
    const body = request.body
×
UNCOV
60
    const invoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(body.payment_hash)
×
UNCOV
61
    const storedInvoice = await this.invoiceRepository.findById(body.payment_hash)
×
62

UNCOV
63
    if (!storedInvoice) {
×
UNCOV
64
      response.status(404).setHeader('content-type', 'text/plain; charset=utf8').send('No such invoice')
×
UNCOV
65
      return
×
66
    }
67

UNCOV
68
    try {
×
UNCOV
69
      await this.paymentsService.updateInvoice(invoice)
×
70
    } catch (error) {
UNCOV
71
      logger.error(`Unable to persist invoice ${invoice.id}`, error)
×
72

UNCOV
73
      throw error
×
74
    }
75

UNCOV
76
    if (invoice.status !== InvoiceStatus.COMPLETED && !invoice.confirmedAt) {
×
UNCOV
77
      response.status(200).send()
×
78

UNCOV
79
      return
×
80
    }
81

UNCOV
82
    if (storedInvoice.status === InvoiceStatus.COMPLETED) {
×
UNCOV
83
      response.status(409).setHeader('content-type', 'text/plain; charset=utf8').send('Invoice is already marked paid')
×
UNCOV
84
      return
×
85
    }
86

UNCOV
87
    invoice.amountPaid = invoice.amountRequested
×
88

UNCOV
89
    try {
×
UNCOV
90
      await this.paymentsService.confirmInvoice(invoice as Invoice)
×
UNCOV
91
      await this.paymentsService.sendInvoiceUpdateNotification(invoice as Invoice)
×
92
    } catch (error) {
UNCOV
93
      logger.error(`Unable to confirm invoice ${invoice.id}`, error)
×
94

UNCOV
95
      throw error
×
96
    }
97

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

© 2026 Coveralls, Inc