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.
@@ -1,10 +1,11 @@
1
1
  // src/index.ts
2
+ import { readHarnessUsage } from "@pooriaarab/vibe-core";
2
3
  import { createConsentLedger as createConsentLedger2 } from "@pooriaarab/vibe-core";
3
4
 
4
5
  // src/p2p.ts
5
- import { createHash, randomBytes } from "crypto";
6
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
7
- import path3 from "path";
6
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
7
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
8
+ import path4 from "path";
8
9
  import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
9
10
 
10
11
  // src/frame.ts
@@ -39,7 +40,27 @@ function parseFrame(raw) {
39
40
  case "hello": {
40
41
  const { handle, league: league2, harness } = r;
41
42
  if (typeof handle !== "string" || typeof league2 !== "string") return null;
42
- return { t: "hello", handle, league: league2, harness: typeof harness === "string" ? harness : "unknown" };
43
+ const verified = r["verified"];
44
+ if (verified !== void 0 && typeof verified !== "boolean") return null;
45
+ const pubkey = r["pubkey"];
46
+ if (pubkey !== void 0 && (typeof pubkey !== "string" || !/^[0-9a-fA-F]{64}$/.test(pubkey)))
47
+ return null;
48
+ const nonce = r["nonce"];
49
+ if (nonce !== void 0 && (typeof nonce !== "string" || !/^[0-9a-fA-F]{1,64}$/.test(nonce)))
50
+ return null;
51
+ const sig = r["sig"];
52
+ if (sig !== void 0 && (typeof sig !== "string" || !/^[0-9a-fA-F]{128}$/.test(sig)))
53
+ return null;
54
+ return {
55
+ t: "hello",
56
+ handle,
57
+ league: league2,
58
+ harness: typeof harness === "string" ? harness : "unknown",
59
+ ...typeof verified === "boolean" ? { verified } : {},
60
+ ...typeof pubkey === "string" ? { pubkey } : {},
61
+ ...typeof nonce === "string" ? { nonce } : {},
62
+ ...typeof sig === "string" ? { sig } : {}
63
+ };
43
64
  }
44
65
  case "msg": {
45
66
  const id = r["id"];
@@ -97,15 +118,286 @@ function parseFrame(raw) {
97
118
  }
98
119
  }
99
120
 
121
+ // src/identity.ts
122
+ import {
123
+ createPrivateKey,
124
+ createPublicKey,
125
+ generateKeyPairSync,
126
+ randomBytes,
127
+ sign,
128
+ verify
129
+ } from "crypto";
130
+ import { chmodSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
131
+ import path2 from "path";
132
+
133
+ // src/state.ts
134
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
135
+ import os from "os";
136
+ import path from "path";
137
+ import { createConsentLedger } from "@pooriaarab/vibe-core";
138
+ var CONSENT_SCOPE = "share:league";
139
+ var LIVE_CONSENT_SCOPE = "share:live";
140
+ function defaultStateDir() {
141
+ return path.join(os.homedir(), ".vibedating");
142
+ }
143
+ var FileConsentStore = class {
144
+ constructor(file) {
145
+ this.file = file;
146
+ }
147
+ file;
148
+ load() {
149
+ try {
150
+ const raw = readFileSync(this.file, "utf8");
151
+ const data = JSON.parse(raw);
152
+ return data.grants ?? [];
153
+ } catch {
154
+ return [];
155
+ }
156
+ }
157
+ save(grants) {
158
+ mkdirSync(path.dirname(this.file), { recursive: true });
159
+ writeFileSync(this.file, JSON.stringify({ grants }, null, 2) + "\n", "utf8");
160
+ }
161
+ };
162
+ function createLedger(dir = defaultStateDir()) {
163
+ return createConsentLedger(new FileConsentStore(path.join(dir, "consent.json")));
164
+ }
165
+ function profilePath(dir) {
166
+ return path.join(dir, "state.json");
167
+ }
168
+ function connectProfile(snapshot, handle, dir = defaultStateDir()) {
169
+ const lg = league(snapshot.totalTokens);
170
+ createLedger(dir).grant(CONSENT_SCOPE, "connect: league bucket only; raw usage stays local");
171
+ const state = {
172
+ handle,
173
+ harness: snapshot.harness,
174
+ league: lg.name,
175
+ leagueMin: lg.min,
176
+ totalTokens: snapshot.totalTokens,
177
+ verified: snapshot.verified,
178
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
179
+ };
180
+ mkdirSync(dir, { recursive: true });
181
+ writeFileSync(profilePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
182
+ return state;
183
+ }
184
+ function loadProfile(dir = defaultStateDir()) {
185
+ try {
186
+ const raw = readFileSync(profilePath(dir), "utf8");
187
+ return JSON.parse(raw);
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ function grantLiveConsent(dir = defaultStateDir()) {
193
+ createLedger(dir).grant(
194
+ LIVE_CONSENT_SCOPE,
195
+ "discover --live: share handle+league+harness+verified flag+identity pubkey (never raw usage) with same-league peers on the public DHT"
196
+ );
197
+ }
198
+ function canShareLive(dir = defaultStateDir()) {
199
+ return createLedger(dir).allows(LIVE_CONSENT_SCOPE);
200
+ }
201
+ var HANDLE_FILE = "handle.json";
202
+ var DEFAULT_HANDLE = "@you";
203
+ var MAX_HANDLE_LEN = 32;
204
+ function handleFilePath(dir) {
205
+ return path.join(dir, HANDLE_FILE);
206
+ }
207
+ function stripLeadingAt(handle) {
208
+ return handle.replace(/^@+/, "");
209
+ }
210
+ function sameHandle(a, b) {
211
+ return stripLeadingAt(a) === stripLeadingAt(b);
212
+ }
213
+ function normalizeHandle(input) {
214
+ if (typeof input !== "string") return null;
215
+ const trimmed = input.trim();
216
+ if (trimmed.length === 0) return null;
217
+ const body = stripLeadingAt(trimmed);
218
+ if (body.length === 0) return null;
219
+ if (/\s/.test(body) || /[\x00-\x1f\x7f]/.test(body)) return null;
220
+ const canonical = "@" + body;
221
+ if (canonical.length > MAX_HANDLE_LEN) return null;
222
+ return canonical;
223
+ }
224
+ function loadHandle(dir = defaultStateDir()) {
225
+ try {
226
+ const raw = readFileSync(handleFilePath(dir), "utf8");
227
+ const data = JSON.parse(raw);
228
+ if (typeof data["handle"] !== "string") return DEFAULT_HANDLE;
229
+ return normalizeHandle(data["handle"]) ?? DEFAULT_HANDLE;
230
+ } catch {
231
+ return DEFAULT_HANDLE;
232
+ }
233
+ }
234
+ function saveHandle(handle, dir = defaultStateDir()) {
235
+ const canonical = normalizeHandle(handle);
236
+ if (canonical === null) {
237
+ throw new Error(
238
+ `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
239
+ );
240
+ }
241
+ mkdirSync(dir, { recursive: true });
242
+ writeFileSync(handleFilePath(dir), JSON.stringify({ handle: canonical }, null, 2) + "\n", "utf8");
243
+ const existing = loadProfile(dir);
244
+ if (existing !== null) {
245
+ writeFileSync(
246
+ profilePath(dir),
247
+ JSON.stringify({ ...existing, handle: canonical }, null, 2) + "\n",
248
+ "utf8"
249
+ );
250
+ }
251
+ return canonical;
252
+ }
253
+ function resolveHandle(dir = defaultStateDir()) {
254
+ const env = process.env["VIBEDATING_HANDLE"];
255
+ if (env !== void 0 && env.trim() !== "") {
256
+ const canonical = normalizeHandle(env);
257
+ if (canonical !== null) return canonical;
258
+ }
259
+ return loadHandle(dir);
260
+ }
261
+ var BLOCKLIST_FILE = "blocklist.json";
262
+ function blocklistPath(dir) {
263
+ return path.join(dir, BLOCKLIST_FILE);
264
+ }
265
+ function persistBlocklist(blocked, dir) {
266
+ mkdirSync(dir, { recursive: true });
267
+ writeFileSync(blocklistPath(dir), JSON.stringify({ blocked }, null, 2) + "\n", "utf8");
268
+ }
269
+ function loadBlocklist(dir = defaultStateDir()) {
270
+ try {
271
+ const raw = readFileSync(blocklistPath(dir), "utf8");
272
+ const data = JSON.parse(raw);
273
+ if (!Array.isArray(data["blocked"])) return [];
274
+ return data["blocked"].filter((h) => typeof h === "string");
275
+ } catch {
276
+ return [];
277
+ }
278
+ }
279
+ function isBlocked(handle, dir = defaultStateDir()) {
280
+ const want = stripLeadingAt(handle);
281
+ if (want === "") return false;
282
+ return loadBlocklist(dir).some((entry) => stripLeadingAt(entry) === want);
283
+ }
284
+ function addBlock(handle, dir = defaultStateDir()) {
285
+ const canonical = normalizeHandle(handle);
286
+ if (canonical === null) {
287
+ throw new Error(
288
+ `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
289
+ );
290
+ }
291
+ const list = loadBlocklist(dir);
292
+ if (list.some((e) => sameHandle(e, canonical))) return { blocked: list, changed: false };
293
+ const blocked = [...list, canonical];
294
+ persistBlocklist(blocked, dir);
295
+ return { blocked, changed: true };
296
+ }
297
+ function removeBlock(handle, dir = defaultStateDir()) {
298
+ const canonical = normalizeHandle(handle);
299
+ if (canonical === null) {
300
+ throw new Error(
301
+ `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
302
+ );
303
+ }
304
+ const list = loadBlocklist(dir);
305
+ const blocked = list.filter((e) => !sameHandle(e, canonical));
306
+ if (blocked.length === list.length) return { blocked: list, changed: false };
307
+ persistBlocklist(blocked, dir);
308
+ return { blocked, changed: true };
309
+ }
310
+
311
+ // src/identity.ts
312
+ var IDENTITY_FILE = "identity.json";
313
+ function identityPath(dir) {
314
+ return path2.join(dir, IDENTITY_FILE);
315
+ }
316
+ function isStoredIdentity(data) {
317
+ if (typeof data !== "object" || data === null) return false;
318
+ const r = data;
319
+ return r["kty"] === "OKP" && r["crv"] === "Ed25519" && typeof r["x"] === "string" && typeof r["d"] === "string" && typeof r["createdAt"] === "string";
320
+ }
321
+ function fromJwk(x, d, createdAt) {
322
+ const privateKey = createPrivateKey({ key: { kty: "OKP", crv: "Ed25519", x, d }, format: "jwk" });
323
+ const publicKey = createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x }, format: "jwk" });
324
+ const publicKeyHex = Buffer.from(x, "base64url").toString("hex");
325
+ if (publicKeyHex.length !== 64) throw new Error("corrupt identity: bad public key length");
326
+ return { publicKeyHex, publicKey, privateKey, createdAt };
327
+ }
328
+ function loadOrCreateIdentity(dir = defaultStateDir()) {
329
+ try {
330
+ const raw = readFileSync2(identityPath(dir), "utf8");
331
+ const data = JSON.parse(raw);
332
+ if (isStoredIdentity(data)) {
333
+ try {
334
+ chmodSync(identityPath(dir), 384);
335
+ } catch {
336
+ }
337
+ return fromJwk(data.x, data.d, data.createdAt);
338
+ }
339
+ } catch {
340
+ }
341
+ const { privateKey } = generateKeyPairSync("ed25519");
342
+ const jwk = privateKey.export({ format: "jwk" });
343
+ if (typeof jwk.x !== "string" || typeof jwk.d !== "string") {
344
+ throw new Error("could not export ed25519 keypair as JWK");
345
+ }
346
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
347
+ const identity = fromJwk(jwk.x, jwk.d, createdAt);
348
+ mkdirSync2(dir, { recursive: true });
349
+ writeFileSync2(
350
+ identityPath(dir),
351
+ JSON.stringify({ kty: "OKP", crv: "Ed25519", x: jwk.x, d: jwk.d, createdAt }, null, 2) + "\n",
352
+ { encoding: "utf8", mode: 384 }
353
+ );
354
+ try {
355
+ chmodSync(identityPath(dir), 384);
356
+ } catch {
357
+ }
358
+ return identity;
359
+ }
360
+ function canonicalHelloClaims(claims) {
361
+ return [claims.handle, claims.league, claims.harness, String(claims.verified === true), claims.nonce].join(
362
+ "|"
363
+ );
364
+ }
365
+ function signHelloClaims(identity, claims) {
366
+ const nonce = randomBytes(16).toString("hex");
367
+ const payload = canonicalHelloClaims({ ...claims, nonce });
368
+ const sig = sign(null, Buffer.from(payload, "utf8"), identity.privateKey).toString("hex");
369
+ return { pubkey: identity.publicKeyHex, nonce, sig };
370
+ }
371
+ function verifyHelloClaims(claims, proof) {
372
+ try {
373
+ if (!/^[0-9a-f]{64}$/i.test(proof.pubkey)) return false;
374
+ if (!/^[0-9a-f]{1,64}$/i.test(proof.nonce)) return false;
375
+ if (!/^[0-9a-f]{128}$/i.test(proof.sig)) return false;
376
+ const publicKey = createPublicKey({
377
+ key: { kty: "OKP", crv: "Ed25519", x: Buffer.from(proof.pubkey, "hex").toString("base64url") },
378
+ format: "jwk"
379
+ });
380
+ const payload = canonicalHelloClaims({ ...claims, nonce: proof.nonce });
381
+ return verify(null, Buffer.from(payload, "utf8"), publicKey, Buffer.from(proof.sig, "hex"));
382
+ } catch {
383
+ return false;
384
+ }
385
+ }
386
+ function classifyHelloIdentity(hello) {
387
+ if (hello.pubkey === void 0) return "legacy";
388
+ if (hello.nonce === void 0 || hello.sig === void 0) return "drop";
389
+ return verifyHelloClaims(hello, { pubkey: hello.pubkey, nonce: hello.nonce, sig: hello.sig }) ? "verified" : "drop";
390
+ }
391
+
100
392
  // src/link.ts
101
393
  import { randomUUID as randomUUID2 } from "crypto";
102
394
 
103
395
  // src/media.ts
104
396
  import { randomUUID } from "crypto";
105
397
  import { readFile } from "fs/promises";
106
- import { writeFileSync } from "fs";
107
- import os from "os";
108
- import path from "path";
398
+ import { writeFileSync as writeFileSync3 } from "fs";
399
+ import os2 from "os";
400
+ import path3 from "path";
109
401
  var DEFAULT_CHUNK_BYTES = Math.floor(MAX_B64_CHUNK_LEN * 3 / 4);
110
402
  var MIME_BY_EXT = {
111
403
  ".png": "image/png",
@@ -117,7 +409,7 @@ var MIME_BY_EXT = {
117
409
  ".webm": "video/webm"
118
410
  };
119
411
  function inferMime(name) {
120
- return MIME_BY_EXT[path.extname(name).toLowerCase()] ?? "application/octet-stream";
412
+ return MIME_BY_EXT[path3.extname(name).toLowerCase()] ?? "application/octet-stream";
121
413
  }
122
414
  function writeFrame(socket, line) {
123
415
  return new Promise((resolve) => {
@@ -148,7 +440,7 @@ async function sendMedia(opts) {
148
440
  }
149
441
  async function sendMediaFile(opts) {
150
442
  const data = await readFile(opts.path);
151
- const name = opts.name ?? path.basename(opts.path);
443
+ const name = opts.name ?? path3.basename(opts.path);
152
444
  const mime = opts.mime ?? inferMime(name);
153
445
  return sendMedia({
154
446
  socket: opts.socket,
@@ -160,7 +452,7 @@ async function sendMediaFile(opts) {
160
452
  });
161
453
  }
162
454
  function safeName(id, name) {
163
- const ext = path.extname(name);
455
+ const ext = path3.extname(name);
164
456
  return ext && /^\.[A-Za-z0-9]{1,16}$/.test(ext) ? `${id}${ext}` : id;
165
457
  }
166
458
  var MediaReceiver = class {
@@ -219,9 +511,9 @@ var MediaReceiver = class {
219
511
  this.transfers.delete(frame.id);
220
512
  if (tx.received !== tx.size) return;
221
513
  const buf = Buffer.concat(tx.chunks, tx.received);
222
- const filePath = path.join(this.opts.tmpDir ?? os.tmpdir(), safeName(frame.id, tx.name));
514
+ const filePath = path3.join(this.opts.tmpDir ?? os2.tmpdir(), safeName(frame.id, tx.name));
223
515
  try {
224
- writeFileSync(filePath, buf);
516
+ writeFileSync3(filePath, buf);
225
517
  } catch {
226
518
  return;
227
519
  }
@@ -364,82 +656,13 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
364
656
  };
365
657
  }
366
658
 
367
- // src/state.ts
368
- import { mkdirSync, readFileSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
369
- import os2 from "os";
370
- import path2 from "path";
371
- import { createConsentLedger } from "@pooriaarab/vibe-core";
372
- var CONSENT_SCOPE = "share:league";
373
- var LIVE_CONSENT_SCOPE = "share:live";
374
- function defaultStateDir() {
375
- return path2.join(os2.homedir(), ".vibedating");
376
- }
377
- var FileConsentStore = class {
378
- constructor(file) {
379
- this.file = file;
380
- }
381
- file;
382
- load() {
383
- try {
384
- const raw = readFileSync(this.file, "utf8");
385
- const data = JSON.parse(raw);
386
- return data.grants ?? [];
387
- } catch {
388
- return [];
389
- }
390
- }
391
- save(grants) {
392
- mkdirSync(path2.dirname(this.file), { recursive: true });
393
- writeFileSync2(this.file, JSON.stringify({ grants }, null, 2) + "\n", "utf8");
394
- }
395
- };
396
- function createLedger(dir = defaultStateDir()) {
397
- return createConsentLedger(new FileConsentStore(path2.join(dir, "consent.json")));
398
- }
399
- function profilePath(dir) {
400
- return path2.join(dir, "state.json");
401
- }
402
- function connectProfile(snapshot, handle, dir = defaultStateDir()) {
403
- const lg = league(snapshot.totalTokens);
404
- createLedger(dir).grant(CONSENT_SCOPE, "connect: league bucket only; raw usage stays local");
405
- const state = {
406
- handle,
407
- harness: snapshot.harness,
408
- league: lg.name,
409
- leagueMin: lg.min,
410
- totalTokens: snapshot.totalTokens,
411
- verified: snapshot.verified,
412
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
413
- };
414
- mkdirSync(dir, { recursive: true });
415
- writeFileSync2(profilePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
416
- return state;
417
- }
418
- function loadProfile(dir = defaultStateDir()) {
419
- try {
420
- const raw = readFileSync(profilePath(dir), "utf8");
421
- return JSON.parse(raw);
422
- } catch {
423
- return null;
424
- }
425
- }
426
- function grantLiveConsent(dir = defaultStateDir()) {
427
- createLedger(dir).grant(
428
- LIVE_CONSENT_SCOPE,
429
- "discover --live: share handle+league+harness (never raw usage) with same-league peers on the public DHT"
430
- );
431
- }
432
- function canShareLive(dir = defaultStateDir()) {
433
- return createLedger(dir).allows(LIVE_CONSENT_SCOPE);
434
- }
435
-
436
659
  // src/p2p.ts
437
660
  var TOPIC_PREFIX = "vibedate:";
438
661
  function leagueTopic(leagueName) {
439
662
  return createHash("sha256").update(`${TOPIC_PREFIX}${leagueName}`, "utf8").digest();
440
663
  }
441
- var LIVE_NOTICE = "live discovery: sharing only your handle + league + harness (never raw usage) with same-league peers on the public DHT";
442
- var MAX_HANDLE_LEN = 64;
664
+ var 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";
665
+ var MAX_HANDLE_LEN2 = 64;
443
666
  var MAX_LEAGUE_LEN = 32;
444
667
  var MAX_HARNESS_LEN = 64;
445
668
  var MAX_HANDSHAKE_LEN = 4096;
@@ -448,7 +671,12 @@ function serializeHandshake(hello) {
448
671
  return JSON.stringify({
449
672
  handle: hello.handle,
450
673
  league: hello.league,
451
- harness: hello.harness
674
+ harness: hello.harness,
675
+ ...hello.verified !== void 0 ? { verified: hello.verified } : {},
676
+ ...hello.pubkey !== void 0 ? { pubkey: hello.pubkey } : {},
677
+ ...hello.nonce !== void 0 ? { nonce: hello.nonce } : {},
678
+ ...hello.sig !== void 0 ? { sig: hello.sig } : {}
679
+ // identityVerified is LOCAL-derived and deliberately never serialized.
452
680
  });
453
681
  }
454
682
  function parseHandshake(raw) {
@@ -464,25 +692,43 @@ function parseHandshake(raw) {
464
692
  const rec = data;
465
693
  const handle = rec["handle"];
466
694
  const league2 = rec["league"];
467
- if (typeof handle !== "string" || handle.length === 0 || handle.length > MAX_HANDLE_LEN) {
695
+ if (typeof handle !== "string" || handle.length === 0 || handle.length > MAX_HANDLE_LEN2) {
468
696
  return null;
469
697
  }
470
698
  if (typeof league2 !== "string" || league2.length === 0 || league2.length > MAX_LEAGUE_LEN) {
471
699
  return null;
472
700
  }
473
701
  const harness = rec["harness"];
702
+ const verified = rec["verified"];
703
+ if (verified !== void 0 && typeof verified !== "boolean") return null;
704
+ const pubkey = rec["pubkey"];
705
+ if (pubkey !== void 0 && (typeof pubkey !== "string" || !/^[0-9a-fA-F]{64}$/.test(pubkey))) {
706
+ return null;
707
+ }
708
+ const nonce = rec["nonce"];
709
+ if (nonce !== void 0 && (typeof nonce !== "string" || !/^[0-9a-fA-F]{1,64}$/.test(nonce))) {
710
+ return null;
711
+ }
712
+ const sig = rec["sig"];
713
+ if (sig !== void 0 && (typeof sig !== "string" || !/^[0-9a-fA-F]{128}$/.test(sig))) {
714
+ return null;
715
+ }
474
716
  return {
475
717
  handle,
476
718
  league: league2,
477
- harness: typeof harness === "string" && harness.length > 0 && harness.length <= MAX_HARNESS_LEN ? harness : "unknown"
719
+ harness: typeof harness === "string" && harness.length > 0 && harness.length <= MAX_HARNESS_LEN ? harness : "unknown",
720
+ ...typeof verified === "boolean" ? { verified } : {},
721
+ ...typeof pubkey === "string" ? { pubkey } : {},
722
+ ...typeof nonce === "string" ? { nonce } : {},
723
+ ...typeof sig === "string" ? { sig } : {}
478
724
  };
479
725
  }
480
726
  function peersPath(dir) {
481
- return path3.join(dir, "peers.json");
727
+ return path4.join(dir, "peers.json");
482
728
  }
483
729
  function loadPeers(dir = defaultStateDir()) {
484
730
  try {
485
- const raw = readFileSync2(peersPath(dir), "utf8");
731
+ const raw = readFileSync3(peersPath(dir), "utf8");
486
732
  const data = JSON.parse(raw);
487
733
  return Array.isArray(data.peers) ? data.peers : [];
488
734
  } catch {
@@ -492,25 +738,53 @@ function loadPeers(dir = defaultStateDir()) {
492
738
  function recordPeer(hello, dir = defaultStateDir(), now = /* @__PURE__ */ new Date()) {
493
739
  const peers = loadPeers(dir);
494
740
  const at = now.toISOString();
495
- const clean = { handle: hello.handle, league: hello.league, harness: hello.harness };
741
+ const clean = {
742
+ handle: hello.handle,
743
+ league: hello.league,
744
+ harness: hello.harness,
745
+ ...hello.verified !== void 0 ? { verified: hello.verified } : {},
746
+ ...hello.pubkey !== void 0 ? { pubkey: hello.pubkey } : {},
747
+ ...hello.identityVerified !== void 0 ? { identityVerified: hello.identityVerified } : {}
748
+ };
496
749
  const existing = peers.findIndex((p) => p.handle === clean.handle);
497
750
  let isNew;
498
751
  let peer;
499
752
  if (existing >= 0) {
500
753
  isNew = false;
501
- peer = { ...peers[existing], ...clean, lastSeenAt: at };
754
+ const prev = peers[existing];
755
+ peer = {
756
+ ...clean,
757
+ firstSeenAt: prev.firstSeenAt,
758
+ lastSeenAt: at,
759
+ // lastMessageAt is local metadata — carried over, never reset by a hello.
760
+ ...prev.lastMessageAt !== void 0 ? { lastMessageAt: prev.lastMessageAt } : {}
761
+ };
502
762
  peers[existing] = peer;
503
763
  } else {
504
764
  isNew = true;
505
765
  peer = { ...clean, firstSeenAt: at, lastSeenAt: at };
506
766
  peers.push(peer);
507
767
  }
508
- mkdirSync2(dir, { recursive: true });
509
- writeFileSync3(peersPath(dir), JSON.stringify({ peers }, null, 2) + "\n", "utf8");
768
+ mkdirSync3(dir, { recursive: true });
769
+ writeFileSync4(peersPath(dir), JSON.stringify({ peers }, null, 2) + "\n", "utf8");
510
770
  return { peer, isNew };
511
771
  }
772
+ function recordPeerMessage(handle, dir = defaultStateDir(), now = /* @__PURE__ */ new Date()) {
773
+ try {
774
+ const peers = loadPeers(dir);
775
+ const idx = peers.findIndex((p) => p.handle === handle);
776
+ if (idx < 0) return false;
777
+ peers[idx] = { ...peers[idx], lastMessageAt: now.toISOString() };
778
+ mkdirSync3(dir, { recursive: true });
779
+ writeFileSync4(peersPath(dir), JSON.stringify({ peers }, null, 2) + "\n", "utf8");
780
+ return true;
781
+ } catch {
782
+ return false;
783
+ }
784
+ }
512
785
  async function startDiscovery(opts) {
513
786
  const { hello, stateDir = defaultStateDir(), onPeer, onLink, notify = vibeCoreNotify } = opts;
787
+ const isBlocked2 = opts.isBlocked;
514
788
  const topics = opts.topics ? [...opts.topics] : opts.topic !== void 0 ? [opts.topic] : [leagueTopic(hello.league)];
515
789
  const acceptLeague = opts.acceptLeague ?? ((l) => l === hello.league);
516
790
  const { default: Hyperswarm } = await import("hyperswarm");
@@ -520,7 +794,16 @@ async function startDiscovery(opts) {
520
794
  swarm.on("connection", (socket, info) => {
521
795
  const remoteKey = info.publicKey.toString("hex");
522
796
  socket.write(
523
- serializeFrame({ t: "hello", handle: hello.handle, league: hello.league, harness: hello.harness }) + "\n"
797
+ serializeFrame({
798
+ t: "hello",
799
+ handle: hello.handle,
800
+ league: hello.league,
801
+ harness: hello.harness,
802
+ ...hello.verified !== void 0 ? { verified: hello.verified } : {},
803
+ ...hello.pubkey !== void 0 ? { pubkey: hello.pubkey } : {},
804
+ ...hello.nonce !== void 0 ? { nonce: hello.nonce } : {},
805
+ ...hello.sig !== void 0 ? { sig: hello.sig } : {}
806
+ }) + "\n"
524
807
  );
525
808
  let buf = "";
526
809
  let handedOff = false;
@@ -535,12 +818,20 @@ async function startDiscovery(opts) {
535
818
  const frame = parseFrame(line);
536
819
  if (frame === null) continue;
537
820
  if (frame.t !== "hello") continue;
821
+ const verdict = classifyHelloIdentity(frame);
822
+ if (verdict === "drop") continue;
538
823
  const peer = {
539
824
  handle: frame.handle,
540
825
  league: frame.league,
541
- harness: frame.harness
826
+ harness: frame.harness,
827
+ ...frame.verified !== void 0 ? { verified: frame.verified } : {},
828
+ ...verdict === "verified" && frame.pubkey !== void 0 ? { pubkey: frame.pubkey, identityVerified: true } : {}
542
829
  };
830
+ if (peer.pubkey !== void 0 && peer.pubkey === hello.pubkey || peer.pubkey === void 0 && hello.pubkey === void 0 && peer.handle === hello.handle) {
831
+ continue;
832
+ }
543
833
  if (!acceptLeague(peer.league)) continue;
834
+ if (isBlocked2 !== void 0 && isBlocked2(peer.handle)) continue;
544
835
  peers.set(remoteKey, peer);
545
836
  const { isNew } = recordPeer(peer, stateDir);
546
837
  if (isNew) {
@@ -561,6 +852,9 @@ async function startDiscovery(opts) {
561
852
  socket.off("data", onData);
562
853
  if (onLink !== void 0) {
563
854
  const link = createPeerLink(socket, peer, buf);
855
+ link.onMessage(() => {
856
+ recordPeerMessage(peer.handle, stateDir);
857
+ });
564
858
  onLink(link);
565
859
  }
566
860
  buf = "";
@@ -646,23 +940,24 @@ function leaguesWithin(name, width) {
646
940
  var TOKENS_ENV = "VIBEDATING_TOKENS";
647
941
  var DEMO_TOTAL_TOKENS = 234e5;
648
942
  var MS_PER_DAY = 864e5;
649
- async function readUsage(harness = "claude-code") {
650
- const verified = await tryReadVerifiedUsage(harness);
651
- if (verified) return verified;
652
- const injected = parseTokensEnv(process.env[TOKENS_ENV]);
653
- const totalTokens = injected ?? DEMO_TOTAL_TOKENS;
943
+ async function readUsage(harness = "claude-code", opts = {}) {
944
+ const merged = { ...opts };
945
+ if (merged.selfReportTokens === void 0) {
946
+ const injected = parseTokensEnv(process.env[TOKENS_ENV]);
947
+ if (injected !== void 0) merged.selfReportTokens = injected;
948
+ }
949
+ const result = await readHarnessUsage(harness, merged);
654
950
  const now = /* @__PURE__ */ new Date();
655
951
  return {
656
952
  harness,
657
- totalTokens,
658
- verified: false,
953
+ totalTokens: result.source === "demo" ? DEMO_TOTAL_TOKENS : result.totalTokens,
954
+ verified: result.source === "real",
955
+ source: result.source,
956
+ ...result.detail !== void 0 ? { detail: result.detail } : {},
659
957
  windowStart: new Date(now.getTime() - 30 * MS_PER_DAY).toISOString(),
660
958
  windowEnd: now.toISOString()
661
959
  };
662
960
  }
663
- async function tryReadVerifiedUsage(_harness) {
664
- return null;
665
- }
666
961
  var TOKEN_MULT = {
667
962
  "": 1,
668
963
  k: 1e3,
@@ -745,6 +1040,19 @@ export {
745
1040
  loadProfile,
746
1041
  grantLiveConsent,
747
1042
  canShareLive,
1043
+ sameHandle,
1044
+ normalizeHandle,
1045
+ saveHandle,
1046
+ resolveHandle,
1047
+ loadBlocklist,
1048
+ isBlocked,
1049
+ addBlock,
1050
+ removeBlock,
1051
+ loadOrCreateIdentity,
1052
+ canonicalHelloClaims,
1053
+ signHelloClaims,
1054
+ verifyHelloClaims,
1055
+ classifyHelloIdentity,
748
1056
  TOPIC_PREFIX,
749
1057
  leagueTopic,
750
1058
  LIVE_NOTICE,
@@ -752,6 +1060,7 @@ export {
752
1060
  parseHandshake,
753
1061
  loadPeers,
754
1062
  recordPeer,
1063
+ recordPeerMessage,
755
1064
  startDiscovery,
756
1065
  LEAGUES,
757
1066
  BELOW_LEAGUE,
@@ -762,7 +1071,6 @@ export {
762
1071
  TOKENS_ENV,
763
1072
  DEMO_TOTAL_TOKENS,
764
1073
  readUsage,
765
- tryReadVerifiedUsage,
766
1074
  parseTokensEnv,
767
1075
  CANDIDATES,
768
1076
  matches,