Skip to content

Commit a85ca44

Browse files
authored
Merge branch 'main' into fix-serialize-javascript
2 parents bb0c9a1 + dfa2838 commit a85ca44

25 files changed

Lines changed: 756 additions & 3 deletions

.changeset/brown-bears-pay.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: maxLimit checks added to subscription message handler
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
Add relay support for the Marmot Protocol (E2EE group messaging over Nostr).
6+
7+
Supported MIPs: 00 (KeyPackages), 01 (Group Construction), 02 (Welcome Events), 03 (Group Messages).
8+
9+
- kind 443 (legacy KeyPackage): stored as a regular event
10+
- kind 10051 (KeyPackage relay list): stored as a replaceable event
11+
- kind 30443 (KeyPackage): stored as a parameterized-replaceable event with `d`-tag deduplication
12+
- kind 444 (Welcome rumor): blocked from direct publishing; must travel inside a kind 1059 gift wrap
13+
- kind 445 (Group Event): dedicated strategy validates the required `h` tag (nostr_group_id) before storing; `#h` tag subscriptions work via the existing generic tag index
14+
- NIP-11 relay info now advertises `supported_mips: [0, 1, 2, 3]`

.changeset/nip-25-reactions.md

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-25 Reactions support for kind 7 and kind 17 events: reaction utility helpers (`isReactionEvent`, `isExternalContentReactionEvent`, `isLikeReaction`, `isDislikeReaction`, `parseReaction`), schema validation enforcing required `e` tag on kind 7 and required `k`/`i` tags on kind 17, unit tests, and integration tests.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ NIPs with a relay-specific implementation are listed here.
5656
- [x] NIP-16: Event Treatment
5757
- [x] NIP-20: Command Results
5858
- [x] NIP-22: Event `created_at` Limits
59+
- [x] NIP-25: Reactions
5960
- [ ] NIP-26: Delegated Event Signing (REMOVED)
6061
- [x] NIP-28: Public Chat
6162
- [x] NIP-33: Parameterized Replaceable Events

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
17,
1919
20,
2020
22,
21+
25,
2122
28,
2223
33,
2324
40,
@@ -26,6 +27,7 @@
2627
65
2728
],
2829
"supportedNipExtensions": [],
30+
"supportedMips": [0, 1, 2, 3],
2931
"main": "src/index.ts",
3032
"bin": {
3133
"nostream": "./dist/src/cli/index.js"

resources/default-settings.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ limits:
181181
- 39999
182182
period: 60000
183183
rate: 24
184+
- description: 60 events/min for Marmot group events (kind 445)
185+
kinds:
186+
- 445
187+
period: 60000
188+
rate: 60
184189
- description: 60 events/min for ephemeral events
185190
kinds:
186191
- - 20000

src/@types/event.ts

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

52+
export type ReactionEntry = {
53+
targetEventId?: string
54+
targetPubkey?: string
55+
targetAddress?: string
56+
targetKind?: number
57+
content: string
58+
}
59+
5260
export type RelayListEntry = {
5361
url: string
5462
marker?: 'read' | 'write'

src/constants/base.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export enum EventKinds {
1111
SEAL = 13,
1212
DIRECT_MESSAGE = 14,
1313
FILE_MESSAGE = 15,
14+
// NIP-25: External content reaction
15+
EXTERNAL_CONTENT_REACTION = 17,
1416
REQUEST_TO_VANISH = 62,
1517
// Channels
1618
CHANNEL_CREATION = 40,
@@ -20,6 +22,10 @@ export enum EventKinds {
2022
CHANNEL_MUTE_USER = 44,
2123
CHANNEL_RESERVED_FIRST = 45,
2224
CHANNEL_RESERVED_LAST = 49,
25+
// Marmot Protocol: E2EE Group Messaging (MIPs)
26+
MARMOT_KEY_PACKAGE_LEGACY = 443, // MIP-00: legacy KeyPackage (regular event, superseded by 30443)
27+
MARMOT_WELCOME_RUMOR = 444, // MIP-02: Welcome rumor (must not be published directly; wraps inside gift wrap)
28+
MARMOT_GROUP_EVENT = 445, // MIP-03: Group Event (proposals, commits, application messages)
2329
// NIP-17: Gift Wrap
2430
GIFT_WRAP = 1059,
2531
// NIP-03: OpenTimestamps attestation
@@ -34,12 +40,16 @@ export enum EventKinds {
3440
REPLACEABLE_FIRST = 10000,
3541
// NIP-65: Relay List Metadata
3642
RELAY_LIST = 10002,
43+
// Marmot Protocol MIP-00: KeyPackage Relay List
44+
MARMOT_KEY_PACKAGE_RELAY_LIST = 10051,
3745
REPLACEABLE_LAST = 19999,
3846
// Ephemeral events
3947
EPHEMERAL_FIRST = 20000,
4048
EPHEMERAL_LAST = 29999,
4149
// Parameterized replaceable events
4250
PARAMETERIZED_REPLACEABLE_FIRST = 30000,
51+
// Marmot Protocol MIP-00: KeyPackage (addressable, replaces legacy 443)
52+
MARMOT_KEY_PACKAGE = 30443,
4353
PARAMETERIZED_REPLACEABLE_LAST = 39999,
4454
USER_APPLICATION_FIRST = 40000,
4555
}
@@ -56,8 +66,14 @@ export enum EventTags {
5666
Invoice = 'bolt11',
5767
// NIP-03: target event kind on an OpenTimestamps attestation
5868
Kind = 'k',
69+
// NIP-25: Reactions
70+
Address = 'a',
71+
Index = 'i',
72+
Emoji = 'emoji',
5973
// NIP-12: geohash tag for location-based queries
6074
Geohash = 'g',
75+
// Marmot Protocol MIP-03: group ID for filtering kind:445 Group Events
76+
Group = 'h',
6177
}
6278

6379
export const ALL_RELAYS = 'ALL_RELAYS'

src/factories/event-strategy-factory.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
isDeleteEvent,
44
isEphemeralEvent,
55
isGiftWrapEvent,
6+
isMarmotGroupEvent,
67
isOpenTimestampsEvent,
78
isParameterizedReplaceableEvent,
89
isReplaceableEvent,
@@ -15,6 +16,7 @@ import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-e
1516
import { Event } from '../@types/event'
1617
import { Factory } from '../@types/base'
1718
import { GiftWrapEventStrategy } from '../handlers/event-strategies/gift-wrap-event-strategy'
19+
import { GroupEventStrategy } from '../handlers/event-strategies/group-event-strategy'
1820
import { IEventStrategy } from '../@types/message-handlers'
1921
import { IWebSocketAdapter } from '../@types/adapters'
2022
import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy'
@@ -32,6 +34,8 @@ export const eventStrategyFactory =
3234
return new VanishEventStrategy(adapter, eventRepository, userRepository)
3335
} else if (isGiftWrapEvent(event)) {
3436
return new GiftWrapEventStrategy(adapter, eventRepository)
37+
} else if (isMarmotGroupEvent(event)) {
38+
return new GroupEventStrategy(adapter, eventRepository)
3539
} else if (isOpenTimestampsEvent(event)) {
3640
return new TimestampEventStrategy(adapter, eventRepository)
3741
} else if (isRelayListEvent(event) || isReplaceableEvent(event)) {
@@ -45,4 +49,4 @@ export const eventStrategyFactory =
4549
}
4650

4751
return new DefaultEventStrategy(adapter, eventRepository)
48-
}
52+
}

src/handlers/event-message-handler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
isFileMessageEvent,
2424
isRequestToVanishEvent,
2525
isSealEvent,
26+
isWelcomeRumorEvent,
2627
} from '../utils/event'
2728
import { IEventRepository, INip05VerificationRepository, IUserRepository } from '../@types/repositories'
2829
import { IEventStrategy, IMessageHandler } from '../@types/message-handlers'
@@ -238,7 +239,9 @@ export class EventMessageHandler implements IMessageHandler {
238239
// NIP-17: kind 13 (Seal) and kind 14 (Direct Message) are inner events that
239240
// must never be published directly to a relay. They are encrypted inside a
240241
// kind 1059 Gift Wrap (NIP-59) before being sent here.
241-
if (isSealEvent(event) || isDirectMessageEvent(event) || isFileMessageEvent(event)) {
242+
// Marmot MIP-02: kind 444 (Welcome rumor) is similarly an inner event that
243+
// must only be delivered inside a kind 1059 gift wrap.
244+
if (isSealEvent(event) || isDirectMessageEvent(event) || isFileMessageEvent(event) || isWelcomeRumorEvent(event)) {
242245
return `blocked: kind ${event.kind} events must not be published directly; wrap them in a kind 1059 gift wrap`
243246
}
244247
}

0 commit comments

Comments
 (0)