vibedate 0.3.0 → 0.4.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
@@ -1,11 +1,16 @@
1
- import { VibeEvent, Harness, UsageSnapshot } from '@pooriaarab/vibe-core';
2
- export { Harness, UsageSnapshot, createConsentLedger } from '@pooriaarab/vibe-core';
1
+ import { VibeEvent, UsageSnapshot, UsageSource, Harness, HarnessUsageOptions } from '@pooriaarab/vibe-core';
2
+ export { Harness, UsageSnapshot, UsageSource, createConsentLedger } from '@pooriaarab/vibe-core';
3
+ import { KeyObject } from 'node:crypto';
3
4
 
4
5
  type Frame = {
5
6
  t: 'hello';
6
7
  handle: string;
7
8
  league: string;
8
9
  harness: string;
10
+ verified?: boolean;
11
+ pubkey?: string;
12
+ nonce?: string;
13
+ sig?: string;
9
14
  } | {
10
15
  t: 'msg';
11
16
  id: string;
@@ -57,11 +62,7 @@ interface ReceivedMedia {
57
62
 
58
63
  interface PeerLink {
59
64
  /** The validated identity of the remote peer (from the hello handshake). */
60
- readonly hello: {
61
- handle: string;
62
- league: string;
63
- harness: string;
64
- };
65
+ readonly hello: PeerHello;
65
66
  /** Send a line of text as a `msg` frame. */
66
67
  send(text: string): void;
67
68
  /** Read a file from disk and send it as a chunked media transfer. */
@@ -99,14 +100,36 @@ declare const TOPIC_PREFIX = "vibedate:";
99
100
  * entire discovery mechanism. Pure.
100
101
  */
101
102
  declare function leagueTopic(leagueName: string): Buffer;
102
- /** The ONLY three fields that ever leave the machine over a peer connection. */
103
+ /**
104
+ * The fields that ever leave the machine over a peer connection: handle, league,
105
+ * harness, and (optionally) the self-asserted usage-verification flag plus the
106
+ * identity proof (pubkey/nonce/sig). NEVER raw usage — no token totals, no logs.
107
+ * `verified`/`pubkey` are undefined for legacy peers that predate them; both
108
+ * `undefined` and `false` display as unverified (~).
109
+ */
103
110
  interface PeerHello {
104
111
  readonly handle: string;
105
112
  readonly league: string;
106
113
  readonly harness: string;
114
+ /**
115
+ * Self-asserted: the sender's usage came from real local logs (see readUsage).
116
+ * Bound to the sender's key by the identity signature when `pubkey` is present.
117
+ */
118
+ readonly verified?: boolean;
119
+ /** Raw ed25519 public key (64 hex) — the persistent identity this hello signs. */
120
+ readonly pubkey?: string;
121
+ /** Random per-hello nonce (hex) covered by the signature. */
122
+ readonly nonce?: string;
123
+ /** ed25519 signature (128 hex) over `handle|league|harness|verified|nonce`. */
124
+ readonly sig?: string;
125
+ /**
126
+ * LOCAL-DERIVED, never on the wire: true when this hello's signature verified
127
+ * against its pubkey (see classifyHelloIdentity). Marked 🔑 in the UI.
128
+ */
129
+ readonly identityVerified?: boolean;
107
130
  }
108
131
  /** One-line privacy notice printed before joining the swarm. */
109
- declare const LIVE_NOTICE = "live discovery: sharing only your handle + league + harness (never raw usage) with same-league peers on the public DHT";
132
+ declare const LIVE_NOTICE = "live discovery: sharing only your handle + league + harness + verified flag + identity pubkey (never raw usage) with same-league peers on the public DHT";
110
133
  /**
111
134
  * Serialize a hello to the single JSON line sent on connect. Built key-by-key
112
135
  * from the allowlist — even if a caller sneaks extra properties onto the
@@ -124,6 +147,8 @@ declare function parseHandshake(raw: string | Buffer): PeerHello | null;
124
147
  interface StoredPeer extends PeerHello {
125
148
  readonly firstSeenAt: string;
126
149
  readonly lastSeenAt: string;
150
+ /** LOCAL metadata: when the last `msg` from this peer arrived (never on the wire). */
151
+ readonly lastMessageAt?: string;
127
152
  }
128
153
  /** Load persisted live peers, or `[]` if none/corrupt. Local-only data. */
129
154
  declare function loadPeers(dir?: string): StoredPeer[];
@@ -135,6 +160,12 @@ declare function recordPeer(hello: PeerHello, dir?: string, now?: Date): {
135
160
  peer: StoredPeer;
136
161
  isNew: boolean;
137
162
  };
163
+ /**
164
+ * Stamp `lastMessageAt` on a stored peer (a `msg` just arrived from them).
165
+ * Local metadata only; never on the wire. Returns false when the handle isn't
166
+ * a known peer. Never throws — best-effort bookkeeping.
167
+ */
168
+ declare function recordPeerMessage(handle: string, dir?: string, now?: Date): boolean;
138
169
  /** Injection point for the match notification (defaults to vibe-core notify). */
139
170
  type NotifySink = (event: VibeEvent) => void;
140
171
  interface DiscoveryOptions {
@@ -160,6 +191,13 @@ interface DiscoveryOptions {
160
191
  * peers that arrive on a shared topic.
161
192
  */
162
193
  readonly acceptLeague?: (peerLeague: string) => boolean;
194
+ /**
195
+ * Predicate over an incoming peer's advertised handle. A blocked peer's hello
196
+ * is DROPPED exactly like a wrong-league one — never recorded to peers.json,
197
+ * never notified, never handed to `onLink`/pairing. Default: nothing blocked.
198
+ * The CLI passes one backed by the persisted blocklist (~/.vibedating).
199
+ */
200
+ readonly isBlocked?: (handle: string) => boolean;
163
201
  /** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
164
202
  readonly bootstrap?: ReadonlyArray<{
165
203
  readonly host: string;
@@ -201,14 +239,74 @@ interface DiscoverySession {
201
239
  declare function startDiscovery(opts: DiscoveryOptions): Promise<DiscoverySession>;
202
240
 
203
241
  /**
204
- * vibedatingdating by token usage.
242
+ * Persistent ed25519 identity binds a handle to a keypair so a peer cannot
243
+ * impersonate it.
205
244
  *
206
- * v0 library surface: league bucketing, local usage reading, and in-league
207
- * matching against a seeded set of candidate profiles. Raw usage never leaves
208
- * the machine; only the league bucket is shared.
245
+ * The keypair lives at `~/.vibedating/identity.json` (mode 0600), generated on
246
+ * first use and reused across runs. A hello that carries a `pubkey` must also
247
+ * carry a valid `sig` over the canonical claims string
248
+ * `handle|league|harness|verified|nonce`; anything else claiming a key is an
249
+ * impersonation attempt and the peer is DROPPED. A hello with no `pubkey` at
250
+ * all is a legacy peer — accepted, but never identity-verified.
209
251
  *
210
- * Builds on `@pooriaarab/vibe-core` (UsageSnapshot / Harness / consent ledger).
252
+ * node:crypto only no new dependency. The private key never leaves the file;
253
+ * only the raw public key (64 hex) and signatures (128 hex) travel on hellos.
254
+ */
255
+
256
+ /** A loaded identity: the raw public key (wire form) plus both key objects. */
257
+ interface Identity {
258
+ /** Raw 32-byte ed25519 public key, hex (64 chars) — the wire form. */
259
+ readonly publicKeyHex: string;
260
+ readonly publicKey: KeyObject;
261
+ readonly privateKey: KeyObject;
262
+ /** ISO timestamp of key generation (first connect). */
263
+ readonly createdAt: string;
264
+ }
265
+ /** The hello fields a signature commits to (a PeerHello minus its proof). */
266
+ interface HelloClaims {
267
+ readonly handle: string;
268
+ readonly league: string;
269
+ readonly harness: string;
270
+ readonly verified?: boolean;
271
+ }
272
+ /** The wire proof attached to a hello: pubkey + nonce + signature, all hex. */
273
+ interface IdentityProof {
274
+ readonly pubkey: string;
275
+ readonly nonce: string;
276
+ readonly sig: string;
277
+ }
278
+ /**
279
+ * Load the persistent keypair, generating + storing it (mode 0600) on first
280
+ * use. A missing or corrupt file is (re)generated — never throws on disk
281
+ * content. Idempotent across runs: same file → same key → same pubkey.
282
+ */
283
+ declare function loadOrCreateIdentity(dir?: string): Identity;
284
+ /**
285
+ * The canonical string an identity signature commits to:
286
+ * `handle|league|harness|verified|nonce` — verified rendered as `true`/`false`.
287
+ * Pure; both sides compute it byte-identically or the signature cannot verify.
288
+ */
289
+ declare function canonicalHelloClaims(claims: HelloClaims & {
290
+ nonce: string;
291
+ }): string;
292
+ /**
293
+ * Sign hello claims with the persistent identity. A fresh random 16-byte nonce
294
+ * per call, so two hellos never share a signature. Returns only the wire proof
295
+ * fields — the private key stays put.
211
296
  */
297
+ declare function signHelloClaims(identity: Identity, claims: HelloClaims): IdentityProof;
298
+ /**
299
+ * Verify a claimed proof against hello claims. NEVER throws — any anomaly
300
+ * (bad hex, bad key, bad signature) is simply `false`.
301
+ */
302
+ declare function verifyHelloClaims(claims: HelloClaims, proof: IdentityProof): boolean;
303
+ /** What an incoming hello's identity material amounts to. */
304
+ type IdentityVerdict = 'verified' | 'legacy' | 'drop';
305
+ /**
306
+ * Classify an incoming hello's identity claim. Pure decision, no IO — the
307
+ * caller (discovery) turns 'drop' into "never recorded, never paired".
308
+ */
309
+ declare function classifyHelloIdentity(hello: HelloClaims & Partial<IdentityProof>): IdentityVerdict;
212
310
 
213
311
  /**
214
312
  * A usage league (volume bucket). `max` is inclusive; the top tier is open-ended
@@ -270,26 +368,34 @@ declare const TOKENS_ENV = "VIBEDATING_TOKENS";
270
368
  */
271
369
  declare const DEMO_TOTAL_TOKENS = 23400000;
272
370
  /**
273
- * v0 usage reader. Resolution order:
274
- *
275
- * 1. (future) read-only OAuth `verified: true`. **NOT implemented in v0**
276
- * see {@link tryReadVerifiedUsage}. This arm is the deliberate seam.
277
- * 2. a self-reported value from the {@link TOKENS_ENV} env var → `verified: false`.
278
- * 3. the demo value {@link DEMO_TOTAL_TOKENS} → `verified: false`.
279
- *
280
- * The snapshot's `totalTokens` is the only thing that must never leave the
281
- * machine; everything downstream consumes only the league bucket.
371
+ * A usage snapshot with honest provenance. `source` says where `totalTokens`
372
+ * came from; `verified` is true ONLY for `source === 'real'` (measured from
373
+ * the harness's own local session logs). The token total is the one thing that
374
+ * must never leave the machine; everything downstream consumes only the league
375
+ * bucket plus the verified flag.
282
376
  */
283
- declare function readUsage(harness?: Harness): Promise<UsageSnapshot>;
377
+ interface LocalUsageSnapshot extends UsageSnapshot {
378
+ /** Where the total came from: measured locally, self-reported, or demo. */
379
+ readonly source: UsageSource;
380
+ /** Short human-facing provenance note from the reader (local display only). */
381
+ readonly detail?: string;
382
+ }
284
383
  /**
285
- * Future home of read-only OAuth verification. Returns `null` in v0.
384
+ * Usage reader, backed by vibe-core's {@link readHarnessUsage}. Resolution order:
385
+ *
386
+ * 1. a self-reported value — explicit `opts.selfReportTokens`, or the
387
+ * {@link TOKENS_ENV} env var (suffix-friendly: `12M`, `1.2B`), or vibe-core's
388
+ * own `VIBE_TOKENS` env → `source: 'self-report'` → `verified: false`.
389
+ * 2. the harness's REAL local session logs (claude-code / codex / gemini / pi /
390
+ * kimi), read from disk by vibe-core → `source: 'real'` → `verified: true`.
391
+ * 3. the demo value {@link DEMO_TOTAL_TOKENS} → `source: 'demo'` →
392
+ * `verified: false`.
286
393
  *
287
- * Implementing this (reading a real, provider-attested usage scope) flips the
288
- * snapshot to `verified: true` and is what makes the league trustworthy. Kept as
289
- * an explicit, named seam rather than a TODO comment so the privacy contract is
290
- * visible at the call site.
394
+ * The demo arm keeps vibedating's own demo total (10M league, non-empty demo
395
+ * pool) rather than vibe-core's smaller placeholder, so demo behavior is
396
+ * unchanged. Raw usage never leaves the machine.
291
397
  */
292
- declare function tryReadVerifiedUsage(_harness: Harness): Promise<UsageSnapshot | null>;
398
+ declare function readUsage(harness?: Harness, opts?: HarnessUsageOptions): Promise<LocalUsageSnapshot>;
293
399
  /**
294
400
  * Parse a self-reported token count: a plain integer (`23400000`), or a suffixed
295
401
  * value (`12M`, `1.2B`, `500k`, `500K`). Returns `undefined` for anything that is
@@ -317,4 +423,4 @@ declare const CANDIDATES: readonly Candidate[];
317
423
  */
318
424
  declare function matches(myLeague: string, candidates?: readonly Candidate[]): Candidate[];
319
425
 
320
- export { BELOW_LEAGUE, CANDIDATES, type Candidate, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, LEAGUES, LIVE_NOTICE, type League, type PeerHello, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, allLeagueNames, league, leagueIndex, leagueTopic, leaguesWithin, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, serializeHandshake, startDiscovery, tryReadVerifiedUsage };
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 };
package/dist/index.js CHANGED
@@ -7,21 +7,26 @@ import {
7
7
  TOKENS_ENV,
8
8
  TOPIC_PREFIX,
9
9
  allLeagueNames,
10
+ canonicalHelloClaims,
11
+ classifyHelloIdentity,
10
12
  createConsentLedger,
11
13
  league,
12
14
  leagueIndex,
13
15
  leagueTopic,
14
16
  leaguesWithin,
17
+ loadOrCreateIdentity,
15
18
  loadPeers,
16
19
  matches,
17
20
  parseHandshake,
18
21
  parseTokensEnv,
19
22
  readUsage,
20
23
  recordPeer,
24
+ recordPeerMessage,
21
25
  serializeHandshake,
26
+ signHelloClaims,
22
27
  startDiscovery,
23
- tryReadVerifiedUsage
24
- } from "./chunk-VBLFAOV3.js";
28
+ verifyHelloClaims
29
+ } from "./chunk-43IYNWES.js";
25
30
  export {
26
31
  BELOW_LEAGUE,
27
32
  CANDIDATES,
@@ -31,18 +36,23 @@ export {
31
36
  TOKENS_ENV,
32
37
  TOPIC_PREFIX,
33
38
  allLeagueNames,
39
+ canonicalHelloClaims,
40
+ classifyHelloIdentity,
34
41
  createConsentLedger,
35
42
  league,
36
43
  leagueIndex,
37
44
  leagueTopic,
38
45
  leaguesWithin,
46
+ loadOrCreateIdentity,
39
47
  loadPeers,
40
48
  matches,
41
49
  parseHandshake,
42
50
  parseTokensEnv,
43
51
  readUsage,
44
52
  recordPeer,
53
+ recordPeerMessage,
45
54
  serializeHandshake,
55
+ signHelloClaims,
46
56
  startDiscovery,
47
- tryReadVerifiedUsage
57
+ verifyHelloClaims
48
58
  };
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runMcp
3
- } from "./chunk-DMA7MSN7.js";
4
- import "./chunk-VBLFAOV3.js";
3
+ } from "./chunk-ZB56ZBAR.js";
4
+ import "./chunk-43IYNWES.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.3.0",
3
+ "version": "0.4.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",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.0.0",
32
- "@pooriaarab/vibe-core": "^0.2.0",
32
+ "@pooriaarab/vibe-core": "^0.3.0",
33
33
  "hyperswarm": "^4.17.0"
34
34
  },
35
35
  "devDependencies": {
@@ -37,7 +37,8 @@
37
37
  "hyperdht": "^6.33.0",
38
38
  "tsup": "^8.0.0",
39
39
  "typescript": "^5.4.0",
40
- "vitest": "^1.6.0"
40
+ "vitest": "^1.6.0",
41
+ "werift": "^0.24.1"
41
42
  },
42
43
  "homepage": "https://github.com/pooriaarab/vibedating#readme",
43
44
  "repository": {