Skip to content

Commit b0fa118

Browse files
authored
Merge branch 'main' into feat/geohash-prefix-filters
2 parents a31e95a + faa7ed2 commit b0fa118

27 files changed

Lines changed: 436 additions & 63 deletions

.changeset/dark-places-tickle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
Fix root HTML negotiation and subpath-aware template links behind trusted proxies.

.changeset/huge-trains-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
Use timingSafeEqual for Nodeless webhook HMAC verification and guard against missing NODELESS_WEBHOOK_SECRET

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,9 @@
153153
"node": ">=24.14.1"
154154
},
155155
"dependencies": {
156-
"@getalby/sdk": "^5.0.0",
157156
"@clack/prompts": "^1.2.0",
157+
"@getalby/sdk": "^5.0.0",
158158
"@noble/secp256k1": "1.7.1",
159-
"accepts": "^1.3.8",
160159
"axios": "^1.15.0",
161160
"cac": "^7.0.0",
162161
"colorette": "^2.0.20",

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/get-invoice.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</head>
1010
<body lang="en">
1111
<main class="container">
12-
<form method="post" action="/invoices">
12+
<form method="post" action="{{path_prefix}}/invoices">
1313
<div class="row">
1414
<div class="col">
1515
<h1 class="mt-4 mb-4 text-center text-nowrap">{{name}}</h1>
@@ -46,7 +46,7 @@ <h1 class="mt-4 mb-4 text-center text-nowrap">{{name}}</h1>
4646
<div class="form-check">
4747
<input class="form-check-input" type="checkbox" id="tosAccepted" name="tosAccepted" value="yes" required>
4848
<label class="form-check-label" for="tosAccepted">
49-
I have read and agree to the <a href="/terms" class="card-link" target="_blank" rel="noopener noreferrer">Terms of Service</a>
49+
I have read and agree to the <a href="{{path_prefix}}/terms" class="card-link" target="_blank" rel="noopener noreferrer">Terms of Service</a>
5050
</label>
5151
</div>
5252
</div>

resources/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ <h5 class="card-title">Admission Required</h5>
4646
This relay requires a one-time admission fee of <strong>{{amount}} sats</strong>
4747
to publish events. Reading events is free.
4848
</p>
49-
<a href="/invoices" class="btn btn-warning">Pay Admission Fee</a>
49+
<a href="{{path_prefix}}/invoices" class="btn btn-warning">Pay Admission Fee</a>
5050
</div>
5151
</div>
5252

@@ -62,9 +62,9 @@ <h5 class="card-title">Open Relay</h5>
6262

6363
<!-- Legal links -->
6464
<div class="d-flex justify-content-center gap-3 mt-2 mb-5">
65-
<a href="/terms" class="text-muted small">Terms of Service</a>
65+
<a href="{{path_prefix}}/terms" class="text-muted small">Terms of Service</a>
6666
<span class="text-muted small">·</span>
67-
<a href="/privacy" class="text-muted small">Privacy Policy</a>
67+
<a href="{{path_prefix}}/privacy" class="text-muted small">Privacy Policy</a>
6868
</div>
6969

7070
</div>

resources/invoices.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</head>
1515
<body lang="en">
1616
<main class="container">
17-
<form method="post" action="/invoices">
17+
<form method="post" action="{{path_prefix}}/invoices">
1818
<div class="row">
1919
<div class="col">
2020
<h1 class="mt-4 mb-4 text-center text-nowrap">{{name}}</h1>
@@ -106,6 +106,7 @@ <h2 class="text-danger">Invoice expired!</h2>
106106
var reference = "{{reference}}"
107107
var relayUrl = "{{relay_url}}"
108108
var relayPubkey = "{{relay_pubkey}}"
109+
var pathPrefix = {{path_prefix_json}};
109110
var invoice = "{{invoice}}";
110111
var pubkey = "{{pubkey}}"
111112
var expiresAt = "{{expires_at}}"
@@ -124,7 +125,7 @@ <h2 class="text-danger">Invoice expired!</h2>
124125
}
125126

126127
async function getInvoiceStatus() {
127-
fetch(`/invoices/${reference}/status`).then(async (response) => {
128+
fetch(`${pathPrefix}/invoices/${reference}/status`).then(async (response) => {
128129
const data = await response.json()
129130
console.log('data', data)
130131
const { status } = data;
@@ -269,4 +270,4 @@ <h2 class="text-danger">Invoice expired!</h2>
269270
document.getElementById('sendPaymentBtn').addEventListener('click', sendPayment)
270271
</script>
271272
</body>
272-
</html>
273+
</html>

resources/post-invoice.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</head>
1515
<body lang="en">
1616
<main class="container">
17-
<form method="post" action="/invoices">
17+
<form method="post" action="{{path_prefix}}/invoices">
1818
<div class="row">
1919
<div class="col">
2020
<h1 class="mt-4 mb-4 text-center text-nowrap">{{name}}</h1>
@@ -106,6 +106,7 @@ <h2 class="text-danger">Invoice expired!</h2>
106106
var reference = {{reference_json}}
107107
var relayUrl = {{relay_url_json}}
108108
var relayPubkey = {{relay_pubkey_json}}
109+
var pathPrefix = {{path_prefix_json}}
109110
var invoice = {{invoice_json}}
110111
var pubkey = {{pubkey_json}}
111112
var expiresAt = {{expires_at_json}}
@@ -124,7 +125,7 @@ <h2 class="text-danger">Invoice expired!</h2>
124125
}
125126

126127
async function getInvoiceStatus() {
127-
fetch(`/invoices/${reference}/status`).then(async (response) => {
128+
fetch(`${pathPrefix}/invoices/${reference}/status`).then(async (response) => {
128129
if (!response.ok) {
129130
throw new Error(`unexpected status ${response.status}`)
130131
}

resources/privacy.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ <h5>Changes to this policy</h5>
6161
</p>
6262

6363
<div class="mt-4">
64-
<a href="/" class="text-muted small">← Back to home</a>
64+
<a href="{{path_prefix}}/" class="text-muted small">← Back to home</a>
6565
<span class="text-muted small mx-2">·</span>
66-
<a href="/terms" class="text-muted small">Terms of Service</a>
66+
<a href="{{path_prefix}}/terms" class="text-muted small">Terms of Service</a>
6767
</div>
6868
</div>
6969
</div>

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

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import { timingSafeEqual } from 'crypto'
2+
13
import { always, applySpec, ifElse, is, path, prop, propEq, propSatisfies } from 'ramda'
24
import { Request, Response } from 'express'
35

46
import { Invoice, InvoiceStatus } from '../../@types/invoice'
57
import { createLogger } from '../../factories/logger-factory'
6-
import { createSettings } from '../../factories/settings-factory'
78
import { fromNodelessInvoice } from '../../utils/transform'
89
import { hmacSha256 } from '../../utils/secret'
910
import { IController } from '../../@types/controllers'
1011
import { IPaymentsService } from '../../@types/services'
11-
import { nodelessCallbackBodySchema } from '../../schemas/nodeless-callback-schema'
12+
import { nodelessCallbackBodySchema, nodelessSignatureSchema } from '../../schemas/nodeless-callback-schema'
1213
import { validateSchema } from '../../utils/validation'
1314

1415
const logger = createLogger('nodeless-callback-controller')
@@ -30,20 +31,31 @@ export class NodelessCallbackController implements IController {
3031
return
3132
}
3233

33-
const settings = createSettings()
34-
const paymentProcessor = settings.payments?.processor
35-
36-
const expected = hmacSha256(process.env.NODELESS_WEBHOOK_SECRET, (request as any).rawBody).toString('hex')
37-
const actual = request.headers['nodeless-signature']
34+
const webhookSecret = process.env.NODELESS_WEBHOOK_SECRET
35+
if (!webhookSecret) {
36+
logger.error('NODELESS_WEBHOOK_SECRET is not configured; unable to verify Nodeless callback')
37+
response
38+
.status(500)
39+
.setHeader('content-type', 'application/json; charset=utf8')
40+
.send('{"status":"error","message":"Internal Server Error"}')
41+
return
42+
}
3843

39-
if (expected !== actual) {
40-
logger.error('nodeless callback request rejected: signature mismatch:', { expected, actual })
41-
response.status(403).send('Forbidden')
44+
const signatureValidation = validateSchema(nodelessSignatureSchema)(request.headers['nodeless-signature'])
45+
if (signatureValidation.error) {
46+
logger('nodeless callback request rejected: invalid signature format')
47+
response
48+
.status(400)
49+
.setHeader('content-type', 'application/json; charset=utf8')
50+
.send('{"status":"error","message":"Invalid signature"}')
4251
return
4352
}
4453

45-
if (paymentProcessor !== 'nodeless') {
46-
logger('denied request from %s to /callbacks/nodeless which is not the current payment processor')
54+
const expectedBuf = hmacSha256(webhookSecret, (request as any).rawBody)
55+
const actualBuf = Buffer.from(signatureValidation.value, 'hex')
56+
57+
if (!timingSafeEqual(expectedBuf, actualBuf)) {
58+
logger('nodeless callback request rejected: signature mismatch')
4759
response.status(403).send('Forbidden')
4860
return
4961
}

0 commit comments

Comments
 (0)