Skip to content

Commit faa7ed2

Browse files
authored
fix: return HTML for browser Accept headers on root route (#532) (#546)
1 parent a6d32b1 commit faa7ed2

22 files changed

Lines changed: 358 additions & 35 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.

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/invoices/get-invoice-controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { FeeSchedule } from '../../@types/settings'
88
import { IController } from '../../@types/controllers'
99

1010
import { getTemplate } from '../../utils/template-cache'
11+
import { getPublicPathPrefix } from '../../utils/http'
1112

1213
export class GetInvoiceController implements IController {
13-
public async handleRequest(_req: Request, res: Response): Promise<void> {
14+
public async handleRequest(req: Request, res: Response): Promise<void> {
1415
const settings = createSettings()
1516

1617
if (
@@ -21,6 +22,7 @@ export class GetInvoiceController implements IController {
2122
const feeSchedule = path<FeeSchedule>(['payments', 'feeSchedules', 'admission', '0'], settings)
2223
const page = getTemplate('./resources/get-invoice.html')
2324
.replaceAll('{{name}}', escapeHtml(name))
25+
.replaceAll('{{path_prefix}}', escapeHtml(getPublicPathPrefix(req, settings)))
2426
.replaceAll('{{processor_json}}', safeJsonForScript(settings.payments.processor))
2527
.replaceAll('{{amount}}', (BigInt(feeSchedule.amount) / 1000n).toString())
2628
.replaceAll('{{nonce}}', res.locals.nonce)

src/controllers/invoices/post-invoice-controller.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { createLogger } from '../../factories/logger-factory'
1414
import { escapeHtml, safeJsonForScript } from '../../utils/html'
1515
import { fromBech32, toBech32 } from '../../utils/transform'
1616
import { getPublicKey, getRelayPrivateKey } from '../../utils/event'
17-
import { getRemoteAddress } from '../../utils/http'
17+
import { getPublicPathPrefix, getRemoteAddress } from '../../utils/http'
1818
import { getTemplate } from '../../utils/template-cache'
1919

2020
const logger = createLogger('post-invoice-controller')
@@ -125,6 +125,7 @@ export class PostInvoiceController implements IController {
125125
const relayPubkey = getPublicKey(relayPrivkey)
126126

127127
const expiresAt = invoice.expiresAt?.toISOString() ?? ''
128+
const pathPrefix = getPublicPathPrefix(request, currentSettings)
128129

129130
const pageContent = getTemplate('./resources/post-invoice.html')
130131
const body = pageContent
@@ -133,6 +134,7 @@ export class PostInvoiceController implements IController {
133134
.replaceAll('{{relay_url_html}}', escapeHtml(relayUrl))
134135
.replaceAll('{{invoice_html}}', escapeHtml(invoice.bolt11))
135136
.replaceAll('{{pubkey_html}}', escapeHtml(pubkey))
137+
.replaceAll('{{path_prefix}}', escapeHtml(pathPrefix))
136138
.replaceAll('{{amount}}', (amount / 1000n).toString())
137139
// JS contexts — safeJsonForScript serializes and escapes < to prevent </script> injection
138140
.replaceAll('{{reference_json}}', safeJsonForScript(invoice.id))
@@ -141,6 +143,7 @@ export class PostInvoiceController implements IController {
141143
.replaceAll('{{invoice_json}}', safeJsonForScript(invoice.bolt11))
142144
.replaceAll('{{pubkey_json}}', safeJsonForScript(pubkey))
143145
.replaceAll('{{expires_at_json}}', safeJsonForScript(expiresAt))
146+
.replaceAll('{{path_prefix_json}}', safeJsonForScript(pathPrefix))
144147
.replaceAll('{{processor_json}}', safeJsonForScript(currentSettings.payments.processor))
145148
// nonce is crypto-random base64 — safe in both attribute and script contexts
146149
.replaceAll('{{nonce}}', response.locals.nonce)

0 commit comments

Comments
 (0)