Skip to content

Commit caf07ea

Browse files
Merge branch 'main' into feat/wot-graph-service
2 parents edece5a + a0853db commit caf07ea

53 files changed

Lines changed: 1976 additions & 296 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
deploy: run production migrations from the relay image
6+
7+
Bake `migrations/` and `knexfile.js` into the runtime image and point the
8+
prod compose migrate service at that image so schema cannot drift from the
9+
code that ships.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
deploy: minimal server bootstrap with optional settings overrides
6+
7+
Add deploy/bootstrap.sh, document what operators must keep locally vs what
8+
ships in the image, and stop seeding a full settings.yaml on first boot so
9+
release defaults merge with optional overrides only.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
fix(ci): publish container image after CI passes on main
6+
7+
Removes the standalone publish workflow and its unsafe workflow_run trigger.
8+
Image publish now runs as the final job in checks.yml on pushes to main, after
9+
lint, build, and tests succeed.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
fix(payments): stop stale invoices from wedging payment polling
6+
7+
A relay could stop clearing payments entirely, needing `delete from invoices` to
8+
recover. Two things combined to cause it.
9+
10+
Invoices created without an expiry could never be retired, because the expiry
11+
check treats a missing date as "not expired", so the maintenance worker left them
12+
pending forever. The LNURL processor set no expiry on any invoice, making this
13+
certain there rather than incidental. Invoices now fall back to
14+
`payments.invoiceExpirySeconds` when the processor reports no expiry of its own,
15+
and existing pending rows without one are backfilled.
16+
17+
Separately, each maintenance pass re-read the same oldest page of pending
18+
invoices, so one page of invoices that never resolve starved every newer one
19+
indefinitely. The worker now advances through the queue and wraps at the end,
20+
keeping the same per-pass cost while guaranteeing every pending invoice is
21+
eventually polled.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
feat(nip43): issue kind 28935 invite codes on request
6+
7+
NIP-43 kind 28935 is not an event clients publish — it is a REQ the relay answers by
8+
minting an invite code on the fly and returning a relay-signed ephemeral event. Nostream
9+
now serves those subscriptions, completing the membership flow: request a claim, join with
10+
kind 28934, publish.
11+
12+
Off by default. It requires `nip43.enabled` and the new `nip43.allowInviteRequests`, a
13+
NIP-42 authenticated requester, an `info.self` consistent with the relay signing key, and a
14+
per-pubkey budget under the new `limits.invite.rateLimits` (5/hour by default). This also
15+
makes the previously inert `nip43.inviteRequestWhitelist` setting take effect. The minted
16+
event is never persisted and never broadcast: the claim tag is a bearer secret and is sent
17+
only to the socket that asked for it.
18+
19+
Two fixes the flow depended on. The relay signs its own events with a key derived from
20+
`SECRET`, but `info.self` was a hand-edited string that nothing validated — by default it
21+
was a placeholder that is not a pubkey at all, so any NIP-43 client verifying a relay-signed
22+
event against `self` would reject it. `info.self` is now optional: when unset or unparseable,
23+
NIP-11 advertises the derived signing pubkey instead, and `nostream info` prints that pubkey
24+
so operators can pin it.
25+
26+
Kind 28935 also sits in the ephemeral range, so a client-published one fell through to
27+
`EphemeralEventStrategy` and was broadcast to every subscriber — including everyone
28+
subscribed to kind 28935 waiting for a real invite. Anyone could inject a forged `claim` tag
29+
into that subscription. It is now rejected with an `OK` false and never broadcast, and
30+
bypasses the NIP-43 admission gate so that rejection actually reaches non-members, who are
31+
the ones most likely to publish it by mistake while trying to obtain a code.
32+
33+
CLI.md and README.md now describe the request flow. CLI.md previously claimed the relay
34+
"does not yet generate kind 28935 on `REQ`", and never mentioned that `nostream info`
35+
prints the signing pubkey that CONFIGURATION.md tells operators to pin.

‎.github/workflows/checks.yml‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,60 @@ jobs:
225225
run: pnpm install --frozen-lockfile
226226
- name: Check for changeset
227227
run: pnpm exec changeset status --since origin/${{ github.base_ref }}
228+
229+
publish-container-image:
230+
name: Build and push container image
231+
runs-on: ubuntu-latest
232+
needs:
233+
- changes
234+
- lint
235+
- build-check
236+
- test-units-and-cover
237+
- test-integrations-and-cover
238+
- post-tests
239+
if: >-
240+
always() && github.event_name == 'push' && github.ref == 'refs/heads/main' &&
241+
needs.changes.result == 'success' &&
242+
(
243+
needs.changes.outputs.src != 'true' ||
244+
(
245+
needs.lint.result == 'success' &&
246+
needs.build-check.result == 'success' &&
247+
needs.test-units-and-cover.result == 'success' &&
248+
needs.test-integrations-and-cover.result == 'success'
249+
)
250+
) &&
251+
needs.post-tests.result == 'success'
252+
env:
253+
REGISTRY: ghcr.io
254+
IMAGE_NAME: ${{ github.repository }}
255+
permissions:
256+
contents: read
257+
packages: write
258+
steps:
259+
- name: Checkout repository
260+
uses: actions/checkout@v4
261+
262+
- name: Log in to the Container registry
263+
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
264+
with:
265+
registry: ${{ env.REGISTRY }}
266+
username: ${{ github.actor }}
267+
password: ${{ secrets.GITHUB_TOKEN }}
268+
269+
- name: Extract metadata (tags, labels) for Docker
270+
id: meta
271+
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
272+
with:
273+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
274+
tags: |
275+
type=raw,value=main
276+
type=sha,prefix=sha-
277+
278+
- name: Build and push Docker image
279+
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
280+
with:
281+
context: .
282+
push: true
283+
tags: ${{ steps.meta.outputs.tags }}
284+
labels: ${{ steps.meta.outputs.labels }}

‎.github/workflows/publish-container-image.yml‎

Lines changed: 0 additions & 57 deletions
This file was deleted.

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ dist
4949
docker-compose.overrides.yml
5050
# Export output
5151
*.jsonl
52+
53+
# Code coverage report output
54+
coverage-report/

‎CLI.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,20 @@ docker compose exec nostream node src/cli/index.js invite create
4646

4747
`--uses` defaults to `nip43.defaultMaxUses` (1). `--expires-in` defaults to `nip43.inviteCodeExpirySeconds` (600 = 10 minutes). `--expires-in` must be a positive integer; never-expiring codes are a yaml policy (`nip43.inviteCodeExpirySeconds: 0`), not a CLI flag. The printed code is the first line of human output so scripts can capture it. If `info.self` is a hex pubkey or `npub1…`, it is stored as `created_by`.
4848

49-
This does not yet generate kind 28935 on `REQ` or publish membership list events.
49+
The relay also answers `REQ`s for kind 28935 by minting a code on the fly and returning it as a relay-signed ephemeral event, so users can obtain a claim string without an operator handing one out. It is off by default and requires `nip43.enabled`, `nip43.allowInviteRequests` and a NIP-42 authenticated client — see [CONFIGURATION.md](CONFIGURATION.md). Membership list events (kind 13534) are not published yet.
50+
51+
Invites minted over `REQ` record the requesting pubkey as `created_by`; `nostream invite create` records `info.self`.
52+
53+
### Relay signing pubkey
54+
55+
NIP-43 clients verify relay-signed events against the `self` field of the NIP-11 document, so `self` must match the key the relay actually signs with. That key is derived from `SECRET` and is otherwise invisible, so `nostream info` prints it:
56+
57+
```bash
58+
nostream info | grep 'Signing pubkey'
59+
nostream info --json # same value under relay.signingPubkey
60+
```
61+
62+
Leave `info.self` unset in settings to have this value advertised automatically. Set it only if you want to pin it explicitly, and set it to exactly this value — a mismatch disables kind 28935 invite requests.
5063

5164
## Removed Legacy Wrappers
5265

‎CONFIGURATION.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ The settings below are listed in alphabetical order by name. Please keep this ta
148148
| info.name | Public name of your relay. (e.g. TBG's Public Relay) |
149149
| info.pubkey | Relay operator's Nostr pubkey in hex format. |
150150
| info.relay_url | Public-facing URL of your relay. (e.g. wss://relay.your-domain.com) |
151-
| info.self | Relay pubkey in hex format for the relay information document `self` field. |
151+
| info.self | Optional. The relay's own signing pubkey (hex or `npub1...`), published as `self` in the relay information document. NIP-43 clients verify relay-signed events against it, so it must match the key the relay signs with. Leave unset to derive it from `SECRET`; run `nostream info` to print the derived value. A configured value that does not match disables kind 28935 invite requests. |
152152
| info.terms_of_service | Public URL to relay terms of service. |
153153
| limits.admissionCheck.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
154154
| limits.admissionCheck.rateLimits[].period | Rate limit period in milliseconds. |
@@ -173,6 +173,8 @@ The settings below are listed in alphabetical order by name. Please keep this ta
173173
| limits.event.retention.pubkey.whitelist | Public keys excluded from retention purge. |
174174
| limits.event.whitelists.ipAddresses | List of IPs (IPv4 or IPv6) to ignore rate limits. |
175175
| limits.event.whitelists.pubkeys | List of public keys to ignore rate limits. |
176+
| limits.invite.rateLimits[].period | Rate limit period in milliseconds for NIP-43 kind 28935 invite requests, counted per requesting pubkey. |
177+
| limits.invite.rateLimits[].rate | Maximum number of invite requests a single pubkey may make during period. Each granted request writes a row to `invite_codes`, so this is the main defence against a code flood. Defaults to 5 per hour. |
176178
| limits.message.ipWhitelist | List of IPs (IPv4 or IPv6) to ignore rate limits. |
177179
| limits.message.rateLimits[].period | Rate limit period in milliseconds. |
178180
| limits.client.subscription.maxSubscriptions | Maximum number of subscriptions per connected client. Defaults to 10. Disabled when set to zero. |
@@ -199,6 +201,8 @@ The settings below are listed in alphabetical order by name. Please keep this ta
199201
| nip43.enabled | Enable NIP-43 invite-based membership. When true, only admitted members may publish. Defaults to false. |
200202
| nip43.inviteCodeExpirySeconds | Seconds until a newly minted invite code expires. `0` means the code never expires. Defaults to 600 (10 minutes). |
201203
| nip43.defaultMaxUses | How many times a newly minted invite code can be claimed. Defaults to 1. |
204+
| nip43.allowInviteRequests | Answer REQs for kind 28935 by minting an invite code on the fly and returning it as a relay-signed ephemeral event on that subscription. NIP-43 requires relays to opt in to this explicitly. Requesters must be authenticated via NIP-42, and `info.self` must match the relay's signing pubkey. Defaults to false. |
205+
| nip43.inviteRequestWhitelist | Public keys allowed to request kind 28935 invite codes. Empty (the default) means any authenticated pubkey may request one; a non-empty list restricts minting to those pubkeys. |
202206
| nip45.enabled | Enable or disable NIP-45 COUNT handling. Defaults to true. |
203207
| nip50.enabled | Enable or disable NIP-50 full-text search. Defaults to false. When enabled, clients can include a `search` field in REQ filters to perform text queries against event content. Requires the GIN full-text index migration. |
204208
| nip50.language | PostgreSQL text-search configuration name. Defaults to `simple` (language-agnostic tokenization). Set to `english`, `spanish`, etc. for stemming support. See [PostgreSQL text search configurations](https://www.postgresql.org/docs/current/textsearch-configuration.html). **Note:** The GIN index migration is built with the `simple` configuration. If you change this value, you must manually rebuild the index: `DROP INDEX CONCURRENTLY events_content_fts_idx; CREATE INDEX CONCURRENTLY events_content_fts_idx ON events USING gin (to_tsvector('<your_language>', event_content));` — otherwise the planner cannot use the index and queries fall back to sequential scans. |

0 commit comments

Comments
 (0)