Skip to content

Commit 919cb8b

Browse files
authored
Merge branch 'main' into fix/nodeless-hmac-timing-safe-comparison
2 parents d4fd38d + ce59383 commit 919cb8b

14 files changed

Lines changed: 330 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
Add NIP-65 Relay List Metadata support for kind 10002 events: relay list utility with `isRelayListEvent` and `parseRelayList` helpers, unit tests, and relay information document updated to advertise NIP-65 (#577).

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ NIPs with a relay-specific implementation are listed here.
6363
- [x] NIP-44: Encrypted Payloads (Versioned)
6464
- [x] NIP-45: Event Counts
6565
- [x] NIP-62: Request to Vanish
66+
- [x] NIP-65: Relay List Metadata
6667

6768
## Requirements
6869

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
33,
2323
40,
2424
44,
25-
45
25+
45,
26+
65
2627
],
2728
"supportedNipExtensions": [],
2829
"main": "src/index.ts",

src/@types/event.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export interface DBEvent {
4949
expires_at?: number
5050
}
5151

52+
export type RelayListEntry = {
53+
url: string
54+
marker?: 'read' | 'write'
55+
}
56+
5257
export interface CanonicalEvent {
5358
0: 0
5459
1: string

src/cli/commands/info.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ const getEventCount = async (): Promise<number | null> => {
5656
}
5757

5858
const getRelayUptimeSeconds = async (): Promise<number | null> => {
59-
const idResult = await runCommandWithOutput('docker', ['compose', 'ps', '-q', 'nostream'], { timeoutMs: 1000 })
59+
let idResult: { code: number; stdout: string; stderr: string }
60+
try {
61+
idResult = await runCommandWithOutput('docker', ['compose', 'ps', '-q', 'nostream'], { timeoutMs: 1000 })
62+
} catch {
63+
return null
64+
}
6065
if (idResult.code !== 0) {
6166
return null
6267
}

src/constants/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export enum EventKinds {
3232
ZAP_RECEIPT = 9735,
3333
// Replaceable events
3434
REPLACEABLE_FIRST = 10000,
35+
// NIP-65: Relay List Metadata
36+
RELAY_LIST = 10002,
3537
REPLACEABLE_LAST = 19999,
3638
// Ephemeral events
3739
EPHEMERAL_FIRST = 20000,

src/factories/event-strategy-factory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isReplaceableEvent,
99
isRequestToVanishEvent,
1010
} from '../utils/event'
11+
import { isRelayListEvent } from '../utils/nip65'
1112
import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy'
1213
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
1314
import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy'
@@ -33,7 +34,7 @@ export const eventStrategyFactory =
3334
return new GiftWrapEventStrategy(adapter, eventRepository)
3435
} else if (isOpenTimestampsEvent(event)) {
3536
return new TimestampEventStrategy(adapter, eventRepository)
36-
} else if (isReplaceableEvent(event)) {
37+
} else if (isRelayListEvent(event) || isReplaceableEvent(event)) {
3738
return new ReplaceableEventStrategy(adapter, eventRepository)
3839
} else if (isEphemeralEvent(event)) {
3940
return new EphemeralEventStrategy(adapter)

src/schemas/event-schema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod'
22

3+
import { EventKinds, EventTags } from '../constants/base'
34
import { createdAtSchema, idSchema, kindSchema, pubkeySchema, signatureSchema, tagSchema } from './base-schema'
45

56
/**
@@ -29,3 +30,16 @@ export const eventSchema = z
2930
sig: signatureSchema,
3031
})
3132
.strict()
33+
.superRefine((event, ctx) => {
34+
if (event.kind === EventKinds.RELAY_LIST) {
35+
event.tags.forEach((tag, index) => {
36+
if (tag[0] === EventTags.Relay && !z.string().url().safeParse(tag[1]).success) {
37+
ctx.addIssue({
38+
code: z.ZodIssueCode.custom,
39+
message: `Invalid relay URL`,
40+
path: ['tags', index, 1],
41+
})
42+
}
43+
})
44+
}
45+
})

src/utils/nip65.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Event, RelayListEntry } from '../@types/event'
2+
import { EventKinds, EventTags } from '../constants/base'
3+
4+
export const isRelayListEvent = (event: Event): boolean => event.kind === EventKinds.RELAY_LIST
5+
6+
export const parseRelayList = (event: Event): RelayListEntry[] =>
7+
event.tags
8+
.filter((tag) => tag[0] === EventTags.Relay && tag.length >= 2)
9+
.map((tag) => ({
10+
url: tag[1],
11+
marker: tag[2] === 'read' || tag[2] === 'write' ? tag[2] : undefined,
12+
}))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Feature: NIP-65 Relay List Metadata
2+
Scenario: Alice publishes a relay list and retrieves it
3+
Given someone called Alice
4+
When Alice sends a relay_list event with relays "wss://alice.relay.com"
5+
And Alice subscribes to her relay_list events
6+
Then Alice receives a relay_list event with relays "wss://alice.relay.com"
7+
8+
Scenario: Alice updates her relay list and only the latest is kept
9+
Given someone called Alice
10+
When Alice sends a relay_list event with relays "wss://old.relay.com"
11+
And Alice sends a relay_list event with relays "wss://new.relay.com"
12+
And Alice subscribes to her relay_list events
13+
Then Alice receives 1 relay_list event and EOSE
14+
And the relay_list event has relays "wss://new.relay.com"
15+
16+
Scenario: Bob can query Alice's relay list
17+
Given someone called Alice
18+
And someone called Bob
19+
When Alice sends a relay_list event with relays "wss://alice.relay.com"
20+
And Bob subscribes to author Alice
21+
Then Bob receives a relay_list event with relays "wss://alice.relay.com"
22+
23+
Scenario: Alice publishes a relay list with read and write markers
24+
Given someone called Alice
25+
When Alice sends a relay_list event with a read relay "wss://read.relay.com" and a write relay "wss://write.relay.com"
26+
And Alice subscribes to her relay_list events
27+
Then Alice receives a relay_list event with a read relay "wss://read.relay.com" and a write relay "wss://write.relay.com"

0 commit comments

Comments
 (0)