vibedate 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -238,6 +238,200 @@ interface DiscoverySession {
238
238
  */
239
239
  declare function startDiscovery(opts: DiscoveryOptions): Promise<DiscoverySession>;
240
240
 
241
+ /**
242
+ * A Nostr event, reduced to the fields this module touches. Structurally
243
+ * compatible with `nostr-tools`'s `Event`, so a finalized/verified event can be
244
+ * published and a relay-delivered event can be received without conversion.
245
+ */
246
+ interface RelayEvent {
247
+ readonly kind: number;
248
+ /** Sender's secp256k1 (Nostr) pubkey, hex — public; the relay sees it. */
249
+ readonly pubkey: string;
250
+ /** Ciphertext for kind-4 DMs; a presence marker for the presence kind. */
251
+ readonly content: string;
252
+ readonly tags: readonly (readonly string[])[];
253
+ readonly created_at: number;
254
+ readonly id: string;
255
+ readonly sig: string;
256
+ }
257
+ /** A subscription filter: kinds + a `#t` conversation-tag match. */
258
+ interface RelayFilter {
259
+ readonly kinds?: readonly number[];
260
+ readonly '#t'?: readonly string[];
261
+ }
262
+ /**
263
+ * The injection point between the relay link and the wire. The production
264
+ * implementation is {@link createNostrPoolTransport} (real wss:// relays via
265
+ * `nostr-tools` `SimplePool`); tests pass an in-memory fan-out. A real relay
266
+ * STORES events and replays matching stored events to new subscribers (that
267
+ * store-and-replay semantics is what makes the presence handshake work across
268
+ * arrival order) — a faithful mock must do the same.
269
+ */
270
+ interface RelayTransport {
271
+ /** Publish a signed event to the relay(s). Fire-and-forget / best-effort. */
272
+ publish(event: RelayEvent): void;
273
+ /**
274
+ * Subscribe to events matching `filter`. Returns an unsubscribe. A faithful
275
+ * relay replays matching STORED events to a brand-new subscriber before any
276
+ * future ones, so the presence handshake is order-independent.
277
+ */
278
+ subscribe(filter: RelayFilter, onEvent: (event: RelayEvent) => void): () => void;
279
+ /** Close all relay connections. Idempotent. */
280
+ close(): void;
281
+ }
282
+ /**
283
+ * Default public Nostr relays (the public commons — we host none of them).
284
+ * Overridable via {@link createNostrPoolTransport}. These are well-known,
285
+ * generally-open relays; any reachable public relay works.
286
+ */
287
+ declare const DEFAULT_RELAYS: readonly string[];
288
+ /** Kind for encrypted DM payloads — the standard NIP-04 sealed-direct-message kind. */
289
+ declare const VIBEDATE_MESSAGE_KIND = 4;
290
+ /**
291
+ * Kind for the presence/rendezvous ping (parameterized-replaceable range). Its
292
+ * content is a constant marker that reveals nothing about the conversation; its
293
+ * `event.pubkey` is the sender's Nostr key (public by definition), and an
294
+ * `ed25519` tag binds it to the sender's identity so the receiver only accepts
295
+ * presence from the peer it expects.
296
+ */
297
+ declare const VIBEDATE_PRESENCE_KIND = 30078;
298
+ /**
299
+ * Derive the deterministic conversation tag two peers rendezvous on, from BOTH
300
+ * ed25519 identity pubkeys (sorted, hashed). Pure: both sides compute it
301
+ * byte-identically from data each already holds (their own identity + the
302
+ * peer's advertised pubkey), so NO extra exchange is needed to agree on the
303
+ * rendezvous point. The hash hides the underlying identities from anyone
304
+ * scanning the relay by tag.
305
+ */
306
+ declare function conversationTag(myEd25519Hex: string, peerEd25519Hex: string): string;
307
+ /**
308
+ * A relay-fallback link with the SAME shape as {@link PeerLink}, so callers can
309
+ * drop it in wherever a direct P2P link is used (pairing, the `live` chat loop).
310
+ *
311
+ * Implemented in v0: `send`/`onMessage`/`onClose`/`close` + `.hello` (text chat
312
+ * over NIP-04). `sendMedia`/`sendSignal`/`onMedia`/`onSignal` are accepted as
313
+ * no-ops so the link is type-interchangeable with a direct PeerLink — chunked
314
+ * media + WebRTC signaling over the relay are `// ponytail:` follow-ups (a relay
315
+ * link is a fallback for peers who can't connect at all, where plain text chat
316
+ * is the priority).
317
+ */
318
+ type NostrRelayLink = PeerLink;
319
+ /** Options for {@link createNostrRelayLink}. */
320
+ interface CreateNostrRelayLinkOptions {
321
+ /** This peer's Nostr secp256k1 keypair (see {@link loadOrCreateNostrKey}). */
322
+ readonly myNostr: {
323
+ readonly sk: Uint8Array;
324
+ readonly pubkey: string;
325
+ };
326
+ /** This peer's ed25519 identity pubkey (hex) — half of the convTag input. */
327
+ readonly myEd25519Hex: string;
328
+ /** The REMOTE peer's ed25519 identity pubkey (hex) — the other convTag half. */
329
+ readonly peerEd25519Hex: string;
330
+ /** The remote peer's identity, surfaced as the link's `.hello`. */
331
+ readonly hello: PeerHello;
332
+ /** The injected relay transport (real pool OR an in-memory mock). */
333
+ readonly transport: RelayTransport;
334
+ }
335
+ /**
336
+ * Build a {@link NostrRelayLink} over the injected {@link RelayTransport}. The
337
+ * link immediately subscribes to the conversation tag and publishes its presence
338
+ * so the peer can learn this side's Nostr pubkey (the bootstrap for NIP-04).
339
+ * Resolves once subscribed + presence published. Async only because
340
+ * `nostr-tools` (NIP-04 + event signing) is lazy-imported on first use.
341
+ */
342
+ declare function createNostrRelayLink(opts: CreateNostrRelayLinkOptions): Promise<NostrRelayLink>;
343
+ /**
344
+ * Build a real {@link RelayTransport} over public Nostr relays via
345
+ * `nostr-tools`'s `SimplePool`. `nostr-tools` (and its WebSocket stack) is
346
+ * imported lazily here. Async because constructing the pool may need the
347
+ * lazy module load. The returned transport:
348
+ * - `publish` is fire-and-forget (publishes to all relays, awaits none);
349
+ * - `subscribe` mirrors `SimplePool.subscribeMany` and returns an unsub;
350
+ * - `close` tears the pool down.
351
+ */
352
+ declare function createNostrPoolTransport(urls?: readonly string[]): Promise<RelayTransport>;
353
+
354
+ /** Namespace prefix so room topics never collide with league topics (or anything
355
+ * else on the DHT). Mirrors {@link p2p.TOPIC_PREFIX} for the 1:1 path. */
356
+ declare const ROOM_TOPIC_PREFIX = "vibedate-room:";
357
+ /**
358
+ * Derive the 32-byte DHT topic for a named room. Deterministic: everyone who
359
+ * joins the same room name anywhere in the world hashes to the same topic,
360
+ * which is the entire discovery mechanism. Pure (mirrors {@link p2p.leagueTopic}).
361
+ */
362
+ declare function roomTopic(name: string): Buffer;
363
+ /** A room member = a connected, handshaken peer. Same shape as a live peer. */
364
+ type RoomMember = PeerHello;
365
+ /** One group chat message: a `msg` frame's payload tagged with the sender. */
366
+ interface RoomMessage {
367
+ /** Sender's handle (from the validated hello — UNTRUSTED display data). */
368
+ readonly from: string;
369
+ readonly id: string;
370
+ readonly text: string;
371
+ readonly at: number;
372
+ }
373
+ interface RoomOptions {
374
+ /** What we broadcast. Must already be consent-gated by the caller. */
375
+ readonly hello: PeerHello;
376
+ /** Room name → its own DHT topic (see {@link roomTopic}). */
377
+ readonly room: string;
378
+ /** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
379
+ readonly bootstrap?: ReadonlyArray<{
380
+ readonly host: string;
381
+ readonly port: number;
382
+ }>;
383
+ /** Where peers.json lives. Defaults to ~/.vibedating. */
384
+ readonly stateDir?: string;
385
+ /** Predicate over a blocked handle — a blocked peer's hello is dropped exactly
386
+ * like in 1:1 discovery. Default: nothing blocked. */
387
+ readonly isBlocked?: (handle: string) => boolean;
388
+ /** Match-notification sink (tests capture with a fake). Best-effort. */
389
+ readonly notify?: NotifySink;
390
+ }
391
+ interface RoomSession {
392
+ /** The room name. */
393
+ readonly room: string;
394
+ /** The joined DHT topic (see {@link roomTopic}). */
395
+ readonly topic: Buffer;
396
+ /** What we broadcast. */
397
+ readonly hello: PeerHello;
398
+ /** Live member set, keyed by handle (excludes self). Mirrors the live view
399
+ * semantics of {@link p2p.DiscoverySession.peers}. */
400
+ readonly members: ReadonlyMap<string, RoomMember>;
401
+ /** Resolves when the first DHT announce/lookup round completes. */
402
+ readonly ready: Promise<unknown>;
403
+ /**
404
+ * Broadcast a text message to ALL room members (fan-out: one `msg` frame per
405
+ * member's {@link PeerLink}). Returns the handles the message was sent to.
406
+ * Best-effort: a member whose link has just closed is skipped silently.
407
+ */
408
+ broadcast(text: string): readonly string[];
409
+ /** Register a callback fired for each incoming group message (with sender). */
410
+ onMessage(cb: (m: RoomMessage) => void): void;
411
+ /** Register a callback fired whenever the member roster changes (join/leave). */
412
+ onRoster(cb: (members: readonly RoomMember[]) => void): void;
413
+ /** Register a callback fired for each incoming `rtc-*` signaling frame,
414
+ * tagged with the sender's handle (full-mesh video signaling). */
415
+ onSignal(cb: (from: string, frame: RtcFrame) => void): void;
416
+ /** Relay one `rtc-*` signaling frame to one member (by handle). */
417
+ sendSignal(handle: string, frame: RtcFrame): void;
418
+ /** The underlying {@link PeerLink} to a member (by handle), or undefined. */
419
+ linkFor(handle: string): PeerLink | undefined;
420
+ /** Leave the room and destroy the node. Idempotent. */
421
+ close(): Promise<void>;
422
+ }
423
+ /**
424
+ * Join (or create) a named room on the DHT and discover ALL members. CONSENT
425
+ * GATE LIVES WITH THE CALLER — never call this without the `share:live` grant
426
+ * (or an explicit opt-in in the same breath), exactly like
427
+ * {@link p2p.startDiscovery}.
428
+ *
429
+ * Resolves once the first DHT announce/lookup round completes; the returned
430
+ * session's {@link RoomSession.members} map + {@link RoomSession.onRoster}
431
+ * callback track every member that joins or leaves thereafter.
432
+ */
433
+ declare function startRoom(opts: RoomOptions): Promise<RoomSession>;
434
+
241
435
  /**
242
436
  * Persistent ed25519 identity — binds a handle to a keypair so a peer cannot
243
437
  * impersonate it.
@@ -307,6 +501,28 @@ type IdentityVerdict = 'verified' | 'legacy' | 'drop';
307
501
  * caller (discovery) turns 'drop' into "never recorded, never paired".
308
502
  */
309
503
  declare function classifyHelloIdentity(hello: HelloClaims & Partial<IdentityProof>): IdentityVerdict;
504
+ /**
505
+ * A separate secp256k1 keypair for the Nostr relay fallback (see relay.ts).
506
+ * Kept DISTINCT from the ed25519 {@link Identity} on purpose: NIP-04 encryption
507
+ * requires a secp256k1 key, and reusing or deriving the ed25519 key for that
508
+ * would cross two unrelated cryptographic curves. The private key is persisted
509
+ * at `<dir>/nostr.json` (mode 0600) and never leaves the file; only the
510
+ * secp256k1 public key (hex) is ever published on a relay.
511
+ */
512
+ interface NostrKey {
513
+ /** 32-byte secp256k1 secret key — the NIP-04 decrypt/encrypt key. Never sent. */
514
+ readonly sk: Uint8Array;
515
+ /** secp256k1 public key, hex (64 chars) — safe to publish; the relay sees it. */
516
+ readonly pubkey: string;
517
+ }
518
+ /**
519
+ * Load the persistent secp256k1 Nostr key, generating + storing it (mode 0600)
520
+ * on first use. A missing or corrupt file is (re)generated — never throws on
521
+ * disk content. Idempotent across runs: same file → same key. `nostr-tools` is
522
+ * imported LAZILY (only when a key must be generated) so non-relay commands
523
+ * never pay for the secp256k1 stack.
524
+ */
525
+ declare function loadOrCreateNostrKey(dir?: string): Promise<NostrKey>;
310
526
 
311
527
  /**
312
528
  * A usage league (volume bucket). `max` is inclusive; the top tier is open-ended
@@ -423,4 +639,4 @@ declare const CANDIDATES: readonly Candidate[];
423
639
  */
424
640
  declare function matches(myLeague: string, candidates?: readonly Candidate[]): Candidate[];
425
641
 
426
- export { BELOW_LEAGUE, CANDIDATES, type Candidate, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, type HelloClaims, type Identity, type IdentityProof, type IdentityVerdict, LEAGUES, LIVE_NOTICE, type League, type LocalUsageSnapshot, type PeerHello, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, allLeagueNames, canonicalHelloClaims, classifyHelloIdentity, league, leagueIndex, leagueTopic, leaguesWithin, loadOrCreateIdentity, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, recordPeerMessage, serializeHandshake, signHelloClaims, startDiscovery, verifyHelloClaims };
642
+ export { BELOW_LEAGUE, CANDIDATES, type Candidate, type CreateNostrRelayLinkOptions, DEFAULT_RELAYS, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, type HelloClaims, type Identity, type IdentityProof, type IdentityVerdict, LEAGUES, LIVE_NOTICE, type League, type LocalUsageSnapshot, type NostrKey, type NostrRelayLink, type PeerHello, ROOM_TOPIC_PREFIX, type RelayEvent, type RelayFilter, type RelayTransport, type RoomMember, type RoomMessage, type RoomOptions, type RoomSession, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, VIBEDATE_MESSAGE_KIND, VIBEDATE_PRESENCE_KIND, allLeagueNames, canonicalHelloClaims, classifyHelloIdentity, conversationTag, createNostrPoolTransport, createNostrRelayLink, league, leagueIndex, leagueTopic, leaguesWithin, loadOrCreateIdentity, loadOrCreateNostrKey, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, recordPeerMessage, roomTopic, serializeHandshake, signHelloClaims, startDiscovery, startRoom, verifyHelloClaims };
package/dist/index.js CHANGED
@@ -1,20 +1,28 @@
1
1
  import {
2
2
  BELOW_LEAGUE,
3
3
  CANDIDATES,
4
+ DEFAULT_RELAYS,
4
5
  DEMO_TOTAL_TOKENS,
5
6
  LEAGUES,
6
7
  LIVE_NOTICE,
8
+ ROOM_TOPIC_PREFIX,
7
9
  TOKENS_ENV,
8
10
  TOPIC_PREFIX,
11
+ VIBEDATE_MESSAGE_KIND,
12
+ VIBEDATE_PRESENCE_KIND,
9
13
  allLeagueNames,
10
14
  canonicalHelloClaims,
11
15
  classifyHelloIdentity,
16
+ conversationTag,
12
17
  createConsentLedger,
18
+ createNostrPoolTransport,
19
+ createNostrRelayLink,
13
20
  league,
14
21
  leagueIndex,
15
22
  leagueTopic,
16
23
  leaguesWithin,
17
24
  loadOrCreateIdentity,
25
+ loadOrCreateNostrKey,
18
26
  loadPeers,
19
27
  matches,
20
28
  parseHandshake,
@@ -22,28 +30,38 @@ import {
22
30
  readUsage,
23
31
  recordPeer,
24
32
  recordPeerMessage,
33
+ roomTopic,
25
34
  serializeHandshake,
26
35
  signHelloClaims,
27
36
  startDiscovery,
37
+ startRoom,
28
38
  verifyHelloClaims
29
- } from "./chunk-KKWP4DLY.js";
39
+ } from "./chunk-HDHJLZZG.js";
30
40
  export {
31
41
  BELOW_LEAGUE,
32
42
  CANDIDATES,
43
+ DEFAULT_RELAYS,
33
44
  DEMO_TOTAL_TOKENS,
34
45
  LEAGUES,
35
46
  LIVE_NOTICE,
47
+ ROOM_TOPIC_PREFIX,
36
48
  TOKENS_ENV,
37
49
  TOPIC_PREFIX,
50
+ VIBEDATE_MESSAGE_KIND,
51
+ VIBEDATE_PRESENCE_KIND,
38
52
  allLeagueNames,
39
53
  canonicalHelloClaims,
40
54
  classifyHelloIdentity,
55
+ conversationTag,
41
56
  createConsentLedger,
57
+ createNostrPoolTransport,
58
+ createNostrRelayLink,
42
59
  league,
43
60
  leagueIndex,
44
61
  leagueTopic,
45
62
  leaguesWithin,
46
63
  loadOrCreateIdentity,
64
+ loadOrCreateNostrKey,
47
65
  loadPeers,
48
66
  matches,
49
67
  parseHandshake,
@@ -51,8 +69,10 @@ export {
51
69
  readUsage,
52
70
  recordPeer,
53
71
  recordPeerMessage,
72
+ roomTopic,
54
73
  serializeHandshake,
55
74
  signHelloClaims,
56
75
  startDiscovery,
76
+ startRoom,
57
77
  verifyHelloClaims
58
78
  };
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runMcp
3
- } from "./chunk-I5U3O4RH.js";
4
- import "./chunk-KKWP4DLY.js";
3
+ } from "./chunk-ZD6JBWEW.js";
4
+ import "./chunk-HDHJLZZG.js";
5
5
  export {
6
6
  runMcp
7
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,8 @@
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.0.0",
32
32
  "@pooriaarab/vibe-core": "^0.3.0",
33
- "hyperswarm": "^4.17.0"
33
+ "hyperswarm": "^4.17.0",
34
+ "nostr-tools": "^2.24.1"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^20.11.0",