1+ import { timingSafeEqual } from 'crypto'
2+
13import { Request , Response } from 'express'
24
35import { Invoice , InvoiceStatus } from '../../@types/invoice'
46import { createLogger } from '../../factories/logger-factory'
5- import { fromOpenNodeInvoice } from '../../utils/transform'
7+ import { createSettings } from '../../factories/settings-factory'
8+ import { getRemoteAddress } from '../../utils/http'
9+ import { hmacSha256 } from '../../utils/secret'
610import { IController } from '../../@types/controllers'
711import { IPaymentsService } from '../../@types/services'
812
@@ -13,15 +17,79 @@ export class OpenNodeCallbackController implements IController {
1317 private readonly paymentsService : IPaymentsService ,
1418 ) { }
1519
16- // TODO: Validate
1720 public async handleRequest (
1821 request : Request ,
1922 response : Response ,
2023 ) {
2124 debug ( 'request headers: %o' , request . headers )
2225 debug ( 'request body: %O' , request . body )
2326
24- const invoice = fromOpenNodeInvoice ( request . body )
27+ const settings = createSettings ( )
28+ const remoteAddress = getRemoteAddress ( request , settings )
29+ const paymentProcessor = settings . payments ?. processor
30+
31+ if ( paymentProcessor !== 'opennode' ) {
32+ debug ( 'denied request from %s to /callbacks/opennode which is not the current payment processor' , remoteAddress )
33+ response
34+ . status ( 403 )
35+ . send ( 'Forbidden' )
36+ return
37+ }
38+
39+ const validStatuses = [ 'expired' , 'refunded' , 'unpaid' , 'processing' , 'underpaid' , 'paid' ]
40+
41+ if (
42+ ! request . body
43+ || typeof request . body . id !== 'string'
44+ || typeof request . body . hashed_order !== 'string'
45+ || typeof request . body . status !== 'string'
46+ || ! validStatuses . includes ( request . body . status )
47+ ) {
48+ response
49+ . status ( 400 )
50+ . setHeader ( 'content-type' , 'text/plain; charset=utf8' )
51+ . send ( 'Bad Request' )
52+ return
53+ }
54+
55+ const openNodeApiKey = process . env . OPENNODE_API_KEY
56+ if ( ! openNodeApiKey ) {
57+ debug ( 'OPENNODE_API_KEY is not configured; unable to verify OpenNode callback from %s' , remoteAddress )
58+ response
59+ . status ( 500 )
60+ . setHeader ( 'content-type' , 'text/plain; charset=utf8' )
61+ . send ( 'Internal Server Error' )
62+ return
63+ }
64+
65+ const expectedBuf = hmacSha256 ( openNodeApiKey , request . body . id )
66+ const actualHex = request . body . hashed_order
67+ const actualBuf = Buffer . from ( actualHex , 'hex' )
68+
69+ if (
70+ expectedBuf . length !== actualBuf . length
71+ || ! timingSafeEqual ( expectedBuf , actualBuf )
72+ ) {
73+ debug ( 'unauthorized request from %s to /callbacks/opennode: hashed_order mismatch' , remoteAddress )
74+ response
75+ . status ( 403 )
76+ . send ( 'Forbidden' )
77+ return
78+ }
79+
80+ const statusMap : Record < string , InvoiceStatus > = {
81+ expired : InvoiceStatus . EXPIRED ,
82+ refunded : InvoiceStatus . EXPIRED ,
83+ unpaid : InvoiceStatus . PENDING ,
84+ processing : InvoiceStatus . PENDING ,
85+ underpaid : InvoiceStatus . PENDING ,
86+ paid : InvoiceStatus . COMPLETED ,
87+ }
88+
89+ const invoice : Pick < Invoice , 'id' | 'status' > = {
90+ id : request . body . id ,
91+ status : statusMap [ request . body . status ] ,
92+ }
2593
2694 debug ( 'invoice' , invoice )
2795
@@ -34,24 +102,23 @@ export class OpenNodeCallbackController implements IController {
34102 throw error
35103 }
36104
37- if (
38- updatedInvoice . status !== InvoiceStatus . COMPLETED
39- && ! updatedInvoice . confirmedAt
40- ) {
105+ if ( updatedInvoice . status !== InvoiceStatus . COMPLETED ) {
41106 response
42107 . status ( 200 )
43108 . send ( )
44109
45110 return
46111 }
47112
48- invoice . amountPaid = invoice . amountRequested
49- updatedInvoice . amountPaid = invoice . amountRequested
113+ if ( ! updatedInvoice . confirmedAt ) {
114+ updatedInvoice . confirmedAt = new Date ( )
115+ }
116+ updatedInvoice . amountPaid = updatedInvoice . amountRequested
50117
51118 try {
52119 await this . paymentsService . confirmInvoice ( {
53- id : invoice . id ,
54- pubkey : invoice . pubkey ,
120+ id : updatedInvoice . id ,
121+ pubkey : updatedInvoice . pubkey ,
55122 status : updatedInvoice . status ,
56123 amountPaid : updatedInvoice . amountRequested ,
57124 confirmedAt : updatedInvoice . confirmedAt ,
0 commit comments