• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cameri / nostream / 25207542759

01 May 2026 08:07AM UTC coverage: 62.181% (-2.4%) from 64.58%
25207542759

Pull #591

github

web-flow
Merge 29f984796 into d8f62b496
Pull Request #591: fix: add husky install fallback

1679 of 3060 branches covered (54.87%)

Branch coverage included in aggregate %.

3944 of 5983 relevant lines covered (65.92%)

7.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.0
/src/utils/event.ts
1
import * as secp256k1 from '@noble/secp256k1'
2✔
2
import { ALL_RELAYS, EventKinds, EventTags } from '../constants/base'
2✔
3
import { applySpec, pipe, prop } from 'ramda'
2✔
4
import { CanonicalEvent, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event'
5
import { EventId, Pubkey, Tag } from '../@types/base'
6
import cluster from 'cluster'
2✔
7
import { deriveFromSecret } from './secret'
2✔
8
import { EventKindsRange } from '../@types/settings'
9
import { fromBuffer } from './transform'
2✔
10
import { getLeadingZeroBits } from './proof-of-work'
2✔
11
import { isGenericTagQuery, isGeohashPrefixCriterion, stripGeohashPrefixWildcard } from './filter'
2✔
12
import { SubscriptionFilter } from '../@types/subscription'
13
import { WebSocketServerAdapterEvent } from '../constants/adapter'
2✔
14

15
export const serializeEvent = (event: UnidentifiedEvent): CanonicalEvent => [
40✔
16
  0,
17
  event.pubkey,
18
  event.created_at,
19
  event.kind,
20
  event.tags,
21
  event.content,
22
]
23

24
export const toNostrEvent: (event: DBEvent) => Event = applySpec({
2✔
25
  id: pipe(prop('event_id') as () => Buffer, fromBuffer),
26
  kind: prop('event_kind') as () => number,
27
  pubkey: pipe(prop('event_pubkey') as () => Buffer, fromBuffer),
28
  created_at: prop('event_created_at') as () => number,
29
  content: prop('event_content') as () => string,
30
  tags: prop('event_tags') as () => Tag[],
31
  sig: pipe(prop('event_signature') as () => Buffer, fromBuffer),
32
})
33

34
export const isEventKindOrRangeMatch =
2✔
35
  ({ kind }: Event) =>
2✔
36
  (item: EventKinds | EventKindsRange) =>
33✔
37
    typeof item === 'number' ? item === kind : kind >= item[0] && kind <= item[1]
21✔
38

39
export const isEventMatchingFilter =
2✔
40
  (filter: SubscriptionFilter) =>
2✔
41
  (event: Event): boolean => {
41✔
42
    const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix)
41✔
43
    const isMatchingGenericTagCriterion = (key: string, criterion: string) => (tag: Tag): boolean => {
41✔
44
      const [, tagName] = key
14✔
45
      if (tag[0] !== tagName) {
14✔
46
        return false
3✔
47
      }
48

49
      if (isGeohashPrefixCriterion(key, criterion)) {
11✔
50
        return tag[1].startsWith(stripGeohashPrefixWildcard(criterion))
2✔
51
      }
52

53
      return tag[1] === criterion
9✔
54
    }
55

56
    // NIP-01: Basic protocol flow description
57

58
    if (Array.isArray(filter.ids) && !filter.ids.some(startsWith(event.id))) {
41✔
59
      return false
4✔
60
    }
61

62
    if (Array.isArray(filter.kinds) && !filter.kinds.includes(event.kind)) {
37✔
63
      return false
3✔
64
    }
65

66
    if (typeof filter.since === 'number' && event.created_at < filter.since) {
34✔
67
      return false
1✔
68
    }
69

70
    if (typeof filter.until === 'number' && event.created_at > filter.until) {
33✔
71
      return false
1✔
72
    }
73

74
    if (Array.isArray(filter.authors)) {
32✔
75
      if (!filter.authors.some(startsWith(event.pubkey))) {
5✔
76
        return false
3✔
77
      }
78
    }
79

80
    // NIP-27: Multicast
81
    // const targetMulticastGroups: string[] = event.tags.reduce(
82
    //   (acc, tag) => (tag[0] === EventTags.Multicast)
83
    //     ? [...acc, tag[1]]
84
    //     : acc,
85
    //   [] as string[]
86
    // )
87

88
    // if (targetMulticastGroups.length && !Array.isArray(filter['#m'])) {
89
    //   return false
90
    // }
91

92
    // NIP-01: Support #e and #p tags
93
    // NIP-12: Support generic tag queries
94

95
    if (
29✔
96
      Object.entries(filter)
97
        .filter(([key, criteria]) => isGenericTagQuery(key) && Array.isArray(criteria))
28✔
98
        .some(([key, criteria]) => {
99
          return !event.tags.some((tag) => criteria.some((criterion) => isMatchingGenericTagCriterion(key, criterion)(tag)))
19✔
100
        })
101
    ) {
102
      return false
8✔
103
    }
104

105
    return true
21✔
106
  }
107

108
export const getEventHash = async (event: Event | UnidentifiedEvent | UnsignedEvent): Promise<string> => {
2✔
109
  const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event))))
39✔
110

111
  return Buffer.from(id).toString('hex')
39✔
112
}
113

114
export const isEventIdValid = async (event: Event): Promise<boolean> => {
2✔
115
  return event.id === (await getEventHash(event))
33✔
116
}
117

118
export const isEventSignatureValid = async (event: Event): Promise<boolean> => {
2✔
119
  return secp256k1.schnorr.verify(event.sig, event.id, event.pubkey)
30✔
120
}
121

122
export const identifyEvent = async (event: UnidentifiedEvent): Promise<UnsignedEvent> => {
2✔
123
  const id = await getEventHash(event)
6✔
124

125
  return { ...event, id }
6✔
126
}
127

128
let privateKeyCache: string | undefined
129
export function getRelayPrivateKey(secret?: string): string {
2✔
130
  if (privateKeyCache) {
63✔
131
    return privateKeyCache
62✔
132
  }
133

134
  if (process.env.RELAY_PRIVATE_KEY) {
1!
135
    privateKeyCache = process.env.RELAY_PRIVATE_KEY
×
136

137
    return privateKeyCache
×
138
  }
139

140
  privateKeyCache = deriveFromSecret(secret).toString('hex')
1✔
141

142
  return privateKeyCache
1✔
143
}
144

145
const publicKeyCache: Record<string, string> = {}
2✔
146
export const getPublicKey = (privkey: string) => {
2✔
147
  if (privkey in publicKeyCache) {
63✔
148
    return publicKeyCache[privkey]
62✔
149
  }
150

151
  publicKeyCache[privkey] = secp256k1.utils.bytesToHex(secp256k1.getPublicKey(privkey, true).subarray(1))
1✔
152

153
  return publicKeyCache[privkey]
1✔
154
}
155

156
export const signEvent =
2✔
157
  (privkey: string | Buffer | undefined) =>
2✔
158
  async (event: UnsignedEvent): Promise<Event> => {
6✔
159
    const sig = await secp256k1.schnorr.sign(event.id, privkey as any)
6✔
160
    return { ...event, sig: Buffer.from(sig).toString('hex') }
6✔
161
  }
162

163
export const broadcastEvent = async (event: Event): Promise<Event> => {
2✔
164
  return new Promise((resolve, reject) => {
×
165
    if (!cluster.isWorker || typeof process.send === 'undefined') {
×
166
      return resolve(event)
×
167
    }
168

169
    process.send(
×
170
      {
171
        eventName: WebSocketServerAdapterEvent.Broadcast,
172
        event,
173
      },
174
      undefined,
175
      undefined,
176
      (error: Error | null) => {
177
        if (error) {
×
178
          return reject(error)
×
179
        }
180
        resolve(event)
×
181
      },
182
    )
183
  })
184
}
185

186
export const isReplaceableEvent = (event: Event): boolean => {
2✔
187
  return (
12✔
188
    event.kind === EventKinds.SET_METADATA ||
44✔
189
    event.kind === EventKinds.CONTACT_LIST ||
190
    event.kind === EventKinds.CHANNEL_METADATA ||
191
    (event.kind >= EventKinds.REPLACEABLE_FIRST && event.kind <= EventKinds.REPLACEABLE_LAST)
192
  )
193
}
194

195
export const isEphemeralEvent = (event: Event): boolean => {
2✔
196
  return event.kind >= EventKinds.EPHEMERAL_FIRST && event.kind <= EventKinds.EPHEMERAL_LAST
7✔
197
}
198

199
export const isParameterizedReplaceableEvent = (event: Event): boolean => {
2✔
200
  return (
6✔
201
    event.kind >= EventKinds.PARAMETERIZED_REPLACEABLE_FIRST && event.kind <= EventKinds.PARAMETERIZED_REPLACEABLE_LAST
11✔
202
  )
203
}
204

205
export const isDeleteEvent = (event: Event): boolean => {
2✔
206
  return event.kind === EventKinds.DELETE
6✔
207
}
208

209
export const isRequestToVanishEvent = (event: Event, relayUrl?: string): boolean => {
2✔
210
  if (event.kind !== EventKinds.REQUEST_TO_VANISH) {
27✔
211
    return false
19✔
212
  }
213

214
  if (typeof relayUrl === 'undefined') {
8✔
215
    return true
3✔
216
  }
217

218
  const relayTags = event.tags.filter((tag) => tag.length >= 2 && tag[0] === EventTags.Relay).map((tag) => tag[1])
5✔
219

220
  return relayTags.length > 0 && relayTags.every((relay) => relay === relayUrl || relay === ALL_RELAYS)
5✔
221
}
222

223
export const isExpiredEvent = (event: Event): boolean => {
2✔
224
  if (!event.tags.length) {
16✔
225
    return false
11✔
226
  }
227

228
  const expirationTime = getEventExpiration(event)
5✔
229

230
  if (!expirationTime) {
5✔
231
    return false
1✔
232
  }
233

234
  const now = Math.floor(new Date().getTime() / 1000)
4✔
235

236
  return expirationTime <= now
4✔
237
}
238

239
export const getEventExpiration = (event: Event): number | undefined => {
2✔
240
  const [, rawExpirationTime] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Expiration) ?? []
19✔
241
  if (!rawExpirationTime) {
19✔
242
    return
11✔
243
  }
244

245
  const expirationTime = Number(rawExpirationTime)
8✔
246

247
  if (Number.isSafeInteger(expirationTime) && Math.log10(expirationTime) < 10) {
8✔
248
    return expirationTime
6✔
249
  }
250
}
251

252
export const getEventProofOfWork = (eventId: EventId): number => {
2✔
253
  return getLeadingZeroBits(Buffer.from(eventId, 'hex'))
2✔
254
}
255

256
export const getPubkeyProofOfWork = (pubkey: Pubkey): number => {
2✔
257
  return getLeadingZeroBits(Buffer.from(pubkey, 'hex'))
2✔
258
}
259

260
// NIP-17: Private Direct Messages helpers
261

262
export const isGiftWrapEvent = (event: Event): boolean => {
2✔
263
  return event.kind === EventKinds.GIFT_WRAP
14✔
264
}
265

266
export const isSealEvent = (event: Event): boolean => {
2✔
267
  return event.kind === EventKinds.SEAL
10✔
268
}
269

270
export const isDirectMessageEvent = (event: Event): boolean => {
2✔
271
  return event.kind === EventKinds.DIRECT_MESSAGE
10✔
272
}
273

274
export const isFileMessageEvent = (event: Event): boolean => {
2✔
275
  return event.kind === EventKinds.FILE_MESSAGE
8✔
276
}
277

278
// NIP-03: OpenTimestamps attestation
279
export const isOpenTimestampsEvent = (event: Event): boolean => {
2✔
280
  return event.kind === EventKinds.OPEN_TIMESTAMPS
9✔
281
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc