Skip to content

Commit bfcdac5

Browse files
authored
feat: Add health api endpoint or kind (#166)
* feat: handle GET /healthz * chore: simplify default settings * chore: keep client alive on message * chore: increase ws heartbeat timeout to 2min * chore: improve logging during startup * fix: zebedee callback crash on expired invoice * fix: QR code csp error * feat: add get-invoice-status-controller * feat: add get-invoice-status-controller factory * chore: refactor router * feat: get invoice status using rest api * fix: bad import Signed-off-by: Ricardo Arturo Cabral Mejía <[email protected]> --------- Signed-off-by: Ricardo Arturo Cabral Mejía <[email protected]>
1 parent 3af6996 commit bfcdac5

File tree

14 files changed

+183
-73
lines changed

14 files changed

+183
-73
lines changed

resources/default-settings.yaml

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@ payments:
1515
whitelists:
1616
pubkeys:
1717
- replace-with-your-pubkey-in-hex
18-
publication:
19-
- enabled: false
20-
description: Publication fee charged per event in msats (1000 msats = 1 satoshi)
21-
amount: 10
22-
whitelists:
23-
pubkeys:
24-
- replace-with-your-pubkey-in-hex
2518
paymentsProcessors:
2619
zebedee:
2720
baseURL: https://api.zebedee.io/
@@ -30,9 +23,8 @@ paymentsProcessors:
3023
- "3.225.112.64"
3124
- "::ffff:3.225.112.64"
3225
network:
33-
maxPayloadSize: 262144
26+
maxPayloadSize: 524288
3427
remoteIpHeader: x-forwarded-for
35-
idleTimeout: 60
3628
workers:
3729
count: 0
3830
mirroring:
@@ -41,25 +33,21 @@ limits:
4133
invoice:
4234
rateLimits:
4335
- period: 60000
44-
rate: 3
36+
rate: 6
4537
- period: 3600000
46-
rate: 10
47-
- period: 86400000
48-
rate: 20
38+
rate: 16
4939
ipWhitelist:
5040
- "::1"
5141
- "10.10.10.1"
5242
- "::ffff:10.10.10.1"
5343
connection:
5444
rateLimits:
5545
- period: 1000
56-
rate: 6
46+
rate: 12
5747
- period: 60000
58-
rate: 30
48+
rate: 48
5949
- period: 3600000
6050
rate: 300
61-
- period: 86400000
62-
rate: 1440
6351
ipWhitelist:
6452
- "::1"
6553
- "10.10.10.1"
@@ -159,25 +147,12 @@ limits:
159147
maxFilters: 10
160148
message:
161149
rateLimits:
162-
# - description: 60 subscriptions/min
163-
# types:
164-
# - REQ
165-
# period: 60000
166-
# rate: 60
167-
# - description: 2880 subscriptions/hour
168-
# types:
169-
# - REQ
170-
# period: 3600000
171-
# rate: 2880
172-
- description: 120 raw messages/min
150+
- description: 240 raw messages/min
173151
period: 60000
174-
rate: 120
152+
rate: 240
175153
- description: 3600 raw messages/hour
176154
period: 3600000
177-
rate: 3600
178-
- description: 86400 raw messages/day
179-
period: 86400000
180-
rate: 86400
155+
rate: 4800
181156
ipWhitelist:
182157
- "::1"
183158
- "10.10.10.1"

resources/invoices.html

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,40 @@ <h2 class="text-danger">Invoice expired!</h2>
106106
var expiresAt = "{{expires_at}}"
107107
var timeout
108108
var paid = false
109+
var fallbackTimeout
110+
111+
function getBackoffTime() {
112+
return 5000 + Math.floor(Math.random() * 5000)
113+
}
114+
115+
async function getInvoiceStatus() {
116+
fetch(`/invoices/${reference}/status`).then(async (response) => {
117+
const data = await response.json()
118+
console.log('data', data)
119+
const { status } = data;
120+
121+
if (status === 'pending') {
122+
fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime())
123+
return
124+
} else if (status === 'expired') {
125+
hide('pending')
126+
show('expired')
127+
return
128+
}
129+
130+
paid = true
131+
132+
clearTimeout(timeout)
133+
134+
hide('pending')
135+
show('paid')
136+
}, (error) => {
137+
console.error('error fetching status', error)
138+
fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime())
139+
})
140+
}
141+
142+
fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime)
109143

110144
function connect() {
111145
var socket = new WebSocket(relayUrl)
@@ -137,22 +171,15 @@ <h2 class="text-danger">Invoice expired!</h2>
137171
}
138172
}
139173
break;
140-
case 'EOSE': {
141-
142-
}
143-
break;
144174
}
145175

146176
if (!paid && message[0] === 'EOSE' && message[1] === 'payment') {
147-
148177
return
149178
}
150179

151180
if (message.length !== 3 || message[0] !== 'EVENT' || message[1] !== 'payment') {
152181
return
153182
}
154-
155-
156183
}
157184

158185
socket.onerror = console.error.bind(console)

src/adapters/web-socket-adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
147147
}
148148

149149
private async onClientMessage(raw: Buffer) {
150+
this.alive = true
150151
let abortable = false
151152
let messageHandler: IMessageHandler & IAbortable | undefined = undefined
152153
try {

src/adapters/web-socket-server-adapter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { WebServerAdapter } from './web-server-adapter'
1414

1515
const debug = createLogger('web-socket-server-adapter')
1616

17-
const WSS_CLIENT_HEALTH_PROBE_INTERVAL = 60000
17+
const WSS_CLIENT_HEALTH_PROBE_INTERVAL = 120000
1818

1919
export class WebSocketServerAdapter extends WebServerAdapter implements IWebSocketServerAdapter {
2020
private webSocketsAdapters: WeakMap<WebSocket, IWebSocketAdapter>
@@ -51,7 +51,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
5151
debug('closing')
5252
clearInterval(this.heartbeatInterval)
5353
this.webSocketServer.clients.forEach((webSocket: WebSocket) => {
54-
debug('terminating client %s', this.webSocketsAdapters.get(webSocket).getClientId())
54+
const webSocketAdapter = this.webSocketsAdapters.get(webSocket)
55+
if (webSocketAdapter) {
56+
debug('terminating client %s: %s', webSocketAdapter.getClientId(), webSocketAdapter.getClientAddress())
57+
}
5558
webSocket.terminate()
5659
})
5760
debug('closing web socket server')

src/app/app.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,16 @@ export class App implements IRunnable {
9393
}
9494
}
9595

96-
logCentered(`${workerCount} workers started`, width)
96+
logCentered(`${workerCount} client workers started`, width)
97+
logCentered('1 maintenance worker started', width)
9798

9899
debug('settings: %O', settings)
99100

100101
const host = `${hostname()}:${port}`
101102
addOnion(torHiddenServicePort, host).then(value=>{
102-
console.info(`tor hidden service address: ${value}:${torHiddenServicePort}`)
103+
logCentered(`Tor hidden service: ${value}:${torHiddenServicePort}`, width)
103104
}, () => {
104-
console.error('Unable to add Tor hidden service. Skipping.')
105+
logCentered('Tor hidden service: disabled', width)
105106
})
106107
}
107108

src/controllers/callbacks/zebedee-callback-controller.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ export class ZebedeeCallbackController implements IController {
1919
response: Response,
2020
) {
2121
debug('request headers: %o', request.headers)
22-
debug('request body: %o', request.body)
22+
debug('request body: %O', request.body)
2323

2424
const invoice = fromZebedeeInvoice(request.body)
2525

2626
debug('invoice', invoice)
2727

2828
try {
29-
await this.paymentsService.updateInvoice(invoice)
29+
if (!invoice.bolt11) {
30+
await this.paymentsService.updateInvoice(invoice)
31+
}
3032
} catch (error) {
3133
console.error(`Unable to persist invoice ${invoice.id}`, error)
3234

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Request, Response } from 'express'
2+
import { IController } from '../../@types/controllers'
3+
import { IInvoiceRepository } from '../../@types/repositories'
4+
5+
export class GetInvoiceStatusController implements IController {
6+
public constructor(
7+
private readonly invoiceRepository: IInvoiceRepository,
8+
) {}
9+
10+
public async handleRequest(
11+
request: Request,
12+
response: Response,
13+
): Promise<void> {
14+
const invoiceId = request.params.invoiceId
15+
if (!invoiceId) {
16+
response
17+
.status(400)
18+
.setHeader('content-type', 'text/plain; charset=utf8')
19+
.send('Invalid invoice')
20+
return
21+
}
22+
23+
try {
24+
const invoice = await this.invoiceRepository.findById(request.params.invoiceId)
25+
26+
if (!invoice) {
27+
response
28+
.status(404)
29+
.setHeader('content-type', 'text/plain; charset=utf8')
30+
.send('Invoice not found')
31+
return
32+
}
33+
34+
response
35+
.status(200)
36+
.setHeader('content-type', 'application/json; charset=utf8')
37+
.send(JSON.stringify({
38+
id: invoice.id,
39+
status: invoice.status,
40+
}))
41+
} catch (error) {
42+
console.error(`get-invoice-status-controller: unable to get invoice ${invoiceId}:`, error)
43+
44+
response
45+
.status(500)
46+
.setHeader('content-type', 'text/plain; charset=utf8')
47+
.send('Unable to get invoice status')
48+
}
49+
}
50+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { GetInvoiceStatusController } from '../controllers/invoices/get-invoice-status-controller'
2+
import { getReadReplicaDbClient } from '../database/client'
3+
import { InvoiceRepository } from '../repositories/invoice-repository'
4+
5+
export const createGetInvoiceStatusController = () => {
6+
const rrDbClient = getReadReplicaDbClient()
7+
8+
const invoiceRepository = new InvoiceRepository(rrDbClient)
9+
10+
return new GetInvoiceStatusController(invoiceRepository)
11+
}

src/factories/web-app-factory.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import express from 'express'
2+
import helmet from 'helmet'
3+
4+
import { createLogger } from './logger-factory'
5+
import { createSettings } from './settings-factory'
6+
import { rateLimiterMiddleware } from '../handlers/request-handlers/rate-limiter-middleware'
7+
import router from '../routes'
8+
9+
const debug = createLogger('web-app-factory')
10+
11+
export const createWebApp = () => {
12+
const app = express()
13+
app
14+
.disable('x-powered-by')
15+
.use(rateLimiterMiddleware)
16+
.use((req, res, next) => {
17+
const settings = createSettings()
18+
19+
const relayUrl = new URL(settings.info.relay_url)
20+
const webRelayUrl = new URL(relayUrl.toString())
21+
webRelayUrl.protocol = (relayUrl.protocol === 'wss:') ? 'https:' : ':'
22+
23+
const directives = {
24+
/**
25+
* TODO: Remove 'unsafe-inline'
26+
*/
27+
'img-src': ["'self'", 'data:', 'https://cdn.zebedee.io/an/nostr/'],
28+
'connect-src': ["'self'", settings.info.relay_url as string, webRelayUrl.toString()],
29+
'default-src': ['"self"'],
30+
'script-src-attr': ["'unsafe-inline'"],
31+
'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net/npm/', 'https://unpkg.com/', 'https://cdnjs.cloudflare.com/ajax/libs/'],
32+
'style-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
33+
'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
34+
}
35+
36+
debug('CSP directives: %o', directives)
37+
38+
return helmet.contentSecurityPolicy({ directives })(req, res, next)
39+
})
40+
.use('/favicon.ico', express.static('./resources/favicon.ico'))
41+
.use('/css', express.static('./resources/css'))
42+
.use(router)
43+
44+
return app
45+
}

src/factories/worker-factory.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { is, path, pathSatisfies } from 'ramda'
2-
import express from 'express'
3-
import helmet from 'helmet'
42
import http from 'http'
53
import process from 'process'
64
import { WebSocketServer } from 'ws'
75

86
import { getMasterDbClient, getReadReplicaDbClient } from '../database/client'
97
import { AppWorker } from '../app/worker'
108
import { createSettings } from '../factories/settings-factory'
9+
import { createWebApp } from './web-app-factory'
1110
import { EventRepository } from '../repositories/event-repository'
12-
import { rateLimiterMiddleware } from '../handlers/request-handlers/rate-limiter-middleware'
13-
import router from '../routes'
1411
import { UserRepository } from '../repositories/user-repository'
1512
import { webSocketAdapterFactory } from './websocket-adapter-factory'
1613
import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter'
@@ -23,27 +20,7 @@ export const workerFactory = (): AppWorker => {
2320

2421
const settings = createSettings()
2522

26-
const app = express()
27-
app
28-
.disable('x-powered-by')
29-
.use(rateLimiterMiddleware)
30-
.use(helmet.contentSecurityPolicy({
31-
directives: {
32-
/**
33-
* TODO: Remove 'unsafe-inline'
34-
*/
35-
'img-src': ["'self'", 'https://cdn.zebedee.io/an/nostr/'],
36-
'connect-src': [settings.info.relay_url as string],
37-
'default-src': ['"self"'],
38-
'script-src-attr': ["'unsafe-inline'"],
39-
'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net/npm/', 'https://unpkg.com/', 'https://cdnjs.cloudflare.com/ajax/libs/'],
40-
'style-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
41-
'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
42-
},
43-
}))
44-
.use('/favicon.ico', express.static('./resources/favicon.ico'))
45-
.use('/css', express.static('./resources/css'))
46-
.use(router)
23+
const app = createWebApp()
4724

4825
// deepcode ignore HttpToHttps: we use proxies
4926
const server = http.createServer(app)

0 commit comments

Comments
 (0)