• 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

16.67
/src/controllers/callbacks/nodeless-callback-controller.ts
1
import { always, applySpec, ifElse, is, path, prop, propEq, propSatisfies } from 'ramda'
2✔
2
import { Request, Response } from 'express'
3

4
import { Invoice, InvoiceStatus } from '../../@types/invoice'
2✔
5
import { createLogger } from '../../factories/logger-factory'
2✔
6
import { createSettings } from '../../factories/settings-factory'
2✔
7
import { fromNodelessInvoice } from '../../utils/transform'
2✔
8
import { hmacSha256 } from '../../utils/secret'
2✔
9
import { IController } from '../../@types/controllers'
10
import { IPaymentsService } from '../../@types/services'
11
import { nodelessCallbackBodySchema } from '../../schemas/nodeless-callback-schema'
2✔
12
import { validateSchema } from '../../utils/validation'
2✔
13

14
const debug = createLogger('nodeless-callback-controller')
2✔
15

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

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

NEW
28
    const bodyValidation = validateSchema(nodelessCallbackBodySchema)(request.body)
×
NEW
29
    if (bodyValidation.error) {
×
NEW
30
      debug('nodeless callback request rejected: invalid body %o', bodyValidation.error)
×
NEW
31
      response
×
32
        .status(400)
33
        .setHeader('content-type', 'application/json; charset=utf8')
34
        .send('{"status":"error","message":"Malformed body"}')
NEW
35
      return
×
36
    }
37

38
    const settings = createSettings()
×
39
    const paymentProcessor = settings.payments?.processor
×
40

41
    const expected = hmacSha256(process.env.NODELESS_WEBHOOK_SECRET, (request as any).rawBody).toString('hex')
×
42
    const actual = request.headers['nodeless-signature']
×
43

44
    if (expected !== actual) {
×
45
      console.error('nodeless callback request rejected: signature mismatch:', { expected, actual })
×
46
      response
×
47
        .status(403)
48
        .send('Forbidden')
49
      return
×
50
    }
51

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

60
    const nodelessInvoice = applySpec({
×
61
      id: prop('uuid'),
62
      status: prop('status'),
63
      satsAmount: prop('amount'),
64
      metadata: prop('metadata'),
65
      paidAt: ifElse(
66
        propEq('status', 'paid'),
67
        always(new Date().toISOString()),
68
        always(null),
69
      ),
70
      createdAt: ifElse(
71
        propSatisfies(is(String), 'createdAt'),
72
        prop('createdAt'),
73
        path(['metadata', 'createdAt']),
74
      ),
75
    })(request.body)
76

77
    debug('nodeless invoice: %O', nodelessInvoice)
×
78

79
    const invoice = fromNodelessInvoice(nodelessInvoice)
×
80

81
    debug('invoice: %O', invoice)
×
82

83
    let updatedInvoice: Invoice
84
    try {
×
85
      updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice)
×
86
      debug('updated invoice: %O', updatedInvoice)
×
87
    } catch (error) {
88
      console.error(`Unable to persist invoice ${invoice.id}`, error)
×
89

90
      throw error
×
91
    }
92

93
    if (
×
94
      updatedInvoice.status !== InvoiceStatus.COMPLETED
×
95
      && !updatedInvoice.confirmedAt
96
    ) {
97
      response
×
98
        .status(200)
99
        .send()
100

101
      return
×
102
    }
103

104
    invoice.amountPaid = invoice.amountRequested
×
105
    updatedInvoice.amountPaid = invoice.amountRequested
×
106

107
    try {
×
108
      await this.paymentsService.confirmInvoice(invoice)
×
109
      await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice)
×
110
    } catch (error) {
111
      console.error(`Unable to confirm invoice ${invoice.id}`, error)
×
112

113
      throw error
×
114
    }
115

116
    response
×
117
      .status(200)
118
      .setHeader('content-type', 'application/json; charset=utf8')
119
      .send('{"status":"ok"}')
120
  }
121
}
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