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

cameri / nostream / 24597194438

18 Apr 2026 04:47AM UTC coverage: 49.987% (-0.03%) from 50.013%
24597194438

push

github

web-flow
feat: implement strict validation for payment callbacks (#426)

Fixes #461

Co-authored-by: Ricardo Cabral <me@ricardocabral.io>

497 of 1109 branches covered (44.82%)

Branch coverage included in aggregate %.

21 of 52 new or added lines in 8 files covered. (40.38%)

4 existing lines in 3 files now uncovered.

1435 of 2756 relevant lines covered (52.07%)

9.23 hits per line

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

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

3
import { Invoice, InvoiceStatus } from '../../@types/invoice'
2✔
4
import { createLogger } from '../../factories/logger-factory'
2✔
5
import { createSettings } from '../../factories/settings-factory'
2✔
6
import { fromZebedeeInvoice } from '../../utils/transform'
2✔
7
import { getRemoteAddress } from '../../utils/http'
2✔
8
import { IController } from '../../@types/controllers'
9
import { IPaymentsService } from '../../@types/services'
10
import { validateSchema } from '../../utils/validation'
2✔
11
import { zebedeeCallbackBodySchema } from '../../schemas/zebedee-callback-schema'
2✔
12

13
const debug = createLogger('zebedee-callback-controller')
2✔
14

15
export class ZebedeeCallbackController implements IController {
2✔
16
  public constructor(
17
    private readonly paymentsService: IPaymentsService,
×
18
  ) {}
19

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

NEW
27
    const bodyValidation = validateSchema(zebedeeCallbackBodySchema)(request.body)
×
NEW
28
    if (bodyValidation.error) {
×
NEW
29
      debug('zebedee callback request rejected: invalid body %o', bodyValidation.error)
×
NEW
30
      response
×
31
        .status(400)
32
        .setHeader('content-type', 'text/plain; charset=utf8')
33
        .send('Malformed body')
NEW
34
      return
×
35
    }
36

UNCOV
37
    const settings = createSettings()
×
38

39
    const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {}
×
40
    const remoteAddress = getRemoteAddress(request, settings)
×
41
    const paymentProcessor = settings.payments?.processor
×
42

43
    if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) {
×
44
      debug('unauthorized request from %s to /callbacks/zebedee', remoteAddress)
×
45
      response
×
46
        .status(403)
47
        .send('Forbidden')
48
      return
×
49
    }
50

51
    if (paymentProcessor !== 'zebedee') {
×
52
      debug('denied request from %s to /callbacks/zebedee which is not the current payment processor', remoteAddress)
×
53
      response
×
54
        .status(403)
55
        .send('Forbidden')
56
      return
×
57
    }
58

59
    const invoice = fromZebedeeInvoice(request.body)
×
60

61
    debug('invoice', invoice)
×
62

63
    let updatedInvoice: Invoice
64
    try {
×
65
      updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice)
×
66
    } catch (error) {
67
      console.error(`Unable to persist invoice ${invoice.id}`, error)
×
68

69
      throw error
×
70
    }
71

72
    if (
×
73
      updatedInvoice.status !== InvoiceStatus.COMPLETED
×
74
      && !updatedInvoice.confirmedAt
75
    ) {
76
      response
×
77
        .status(200)
78
        .send()
79

80
      return
×
81
    }
82

83
    invoice.amountPaid = invoice.amountRequested
×
84
    invoice.status = updatedInvoice.status
×
85
    updatedInvoice.amountPaid = invoice.amountRequested
×
86

87
    try {
×
88
      await this.paymentsService.confirmInvoice({
×
89
        id: invoice.id,
90
        pubkey: invoice.pubkey,
91
        status: invoice.status,
92
        confirmedAt: invoice.confirmedAt,
93
        amountPaid: invoice.amountRequested,
94
      })
95
      await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice)
×
96
    } catch (error) {
97
      console.error(`Unable to confirm invoice ${invoice.id}`, error)
×
98

99
      throw error
×
100
    }
101

102
    response
×
103
      .status(200)
104
      .setHeader('content-type', 'text/plain; charset=utf8')
105
      .send('OK')
106
  }
107
}
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