vibedate 0.3.1 → 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,190 +656,12 @@ 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
- var HANDLE_FILE = "handle.json";
436
- var DEFAULT_HANDLE = "@you";
437
- var MAX_HANDLE_LEN = 32;
438
- function handleFilePath(dir) {
439
- return path2.join(dir, HANDLE_FILE);
440
- }
441
- function stripLeadingAt(handle) {
442
- return handle.replace(/^@+/, "");
443
- }
444
- function sameHandle(a, b) {
445
- return stripLeadingAt(a) === stripLeadingAt(b);
446
- }
447
- function normalizeHandle(input) {
448
- if (typeof input !== "string") return null;
449
- const trimmed = input.trim();
450
- if (trimmed.length === 0) return null;
451
- const body = stripLeadingAt(trimmed);
452
- if (body.length === 0) return null;
453
- if (/\s/.test(body) || /[\x00-\x1f\x7f]/.test(body)) return null;
454
- const canonical = "@" + body;
455
- if (canonical.length > MAX_HANDLE_LEN) return null;
456
- return canonical;
457
- }
458
- function loadHandle(dir = defaultStateDir()) {
459
- try {
460
- const raw = readFileSync(handleFilePath(dir), "utf8");
461
- const data = JSON.parse(raw);
462
- if (typeof data["handle"] !== "string") return DEFAULT_HANDLE;
463
- return normalizeHandle(data["handle"]) ?? DEFAULT_HANDLE;
464
- } catch {
465
- return DEFAULT_HANDLE;
466
- }
467
- }
468
- function saveHandle(handle, dir = defaultStateDir()) {
469
- const canonical = normalizeHandle(handle);
470
- if (canonical === null) {
471
- throw new Error(
472
- `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
473
- );
474
- }
475
- mkdirSync(dir, { recursive: true });
476
- writeFileSync2(handleFilePath(dir), JSON.stringify({ handle: canonical }, null, 2) + "\n", "utf8");
477
- const existing = loadProfile(dir);
478
- if (existing !== null) {
479
- writeFileSync2(
480
- profilePath(dir),
481
- JSON.stringify({ ...existing, handle: canonical }, null, 2) + "\n",
482
- "utf8"
483
- );
484
- }
485
- return canonical;
486
- }
487
- function resolveHandle(dir = defaultStateDir()) {
488
- const env = process.env["VIBEDATING_HANDLE"];
489
- if (env !== void 0 && env.trim() !== "") {
490
- const canonical = normalizeHandle(env);
491
- if (canonical !== null) return canonical;
492
- }
493
- return loadHandle(dir);
494
- }
495
- var BLOCKLIST_FILE = "blocklist.json";
496
- function blocklistPath(dir) {
497
- return path2.join(dir, BLOCKLIST_FILE);
498
- }
499
- function persistBlocklist(blocked, dir) {
500
- mkdirSync(dir, { recursive: true });
501
- writeFileSync2(blocklistPath(dir), JSON.stringify({ blocked }, null, 2) + "\n", "utf8");
502
- }
503
- function loadBlocklist(dir = defaultStateDir()) {
504
- try {
505
- const raw = readFileSync(blocklistPath(dir), "utf8");
506
- const data = JSON.parse(raw);
507
- if (!Array.isArray(data["blocked"])) return [];
508
- return data["blocked"].filter((h) => typeof h === "string");
509
- } catch {
510
- return [];
511
- }
512
- }
513
- function isBlocked(handle, dir = defaultStateDir()) {
514
- const want = stripLeadingAt(handle);
515
- if (want === "") return false;
516
- return loadBlocklist(dir).some((entry) => stripLeadingAt(entry) === want);
517
- }
518
- function addBlock(handle, dir = defaultStateDir()) {
519
- const canonical = normalizeHandle(handle);
520
- if (canonical === null) {
521
- throw new Error(
522
- `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
523
- );
524
- }
525
- const list = loadBlocklist(dir);
526
- if (list.some((e) => sameHandle(e, canonical))) return { blocked: list, changed: false };
527
- const blocked = [...list, canonical];
528
- persistBlocklist(blocked, dir);
529
- return { blocked, changed: true };
530
- }
531
- function removeBlock(handle, dir = defaultStateDir()) {
532
- const canonical = normalizeHandle(handle);
533
- if (canonical === null) {
534
- throw new Error(
535
- `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
536
- );
537
- }
538
- const list = loadBlocklist(dir);
539
- const blocked = list.filter((e) => !sameHandle(e, canonical));
540
- if (blocked.length === list.length) return { blocked: list, changed: false };
541
- persistBlocklist(blocked, dir);
542
- return { blocked, changed: true };
543
- }
544
-
545
659
  // src/p2p.ts
546
660
  var TOPIC_PREFIX = "vibedate:";
547
661
  function leagueTopic(leagueName) {
548
662
  return createHash("sha256").update(`${TOPIC_PREFIX}${leagueName}`, "utf8").digest();
549
663
  }
550
- var LIVE_NOTICE = "live discovery: sharing only your handle + league + harness (never raw usage) with same-league peers on the public DHT";
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";
551
665
  var MAX_HANDLE_LEN2 = 64;
552
666
  var MAX_LEAGUE_LEN = 32;
553
667
  var MAX_HARNESS_LEN = 64;
@@ -557,7 +671,12 @@ function serializeHandshake(hello) {
557
671
  return JSON.stringify({
558
672
  handle: hello.handle,
559
673
  league: hello.league,
560
- 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.
561
680
  });
562
681
  }
563
682
  function parseHandshake(raw) {
@@ -580,18 +699,36 @@ function parseHandshake(raw) {
580
699
  return null;
581
700
  }
582
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
+ }
583
716
  return {
584
717
  handle,
585
718
  league: league2,
586
- 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 } : {}
587
724
  };
588
725
  }
589
726
  function peersPath(dir) {
590
- return path3.join(dir, "peers.json");
727
+ return path4.join(dir, "peers.json");
591
728
  }
592
729
  function loadPeers(dir = defaultStateDir()) {
593
730
  try {
594
- const raw = readFileSync2(peersPath(dir), "utf8");
731
+ const raw = readFileSync3(peersPath(dir), "utf8");
595
732
  const data = JSON.parse(raw);
596
733
  return Array.isArray(data.peers) ? data.peers : [];
597
734
  } catch {
@@ -601,23 +738,50 @@ function loadPeers(dir = defaultStateDir()) {
601
738
  function recordPeer(hello, dir = defaultStateDir(), now = /* @__PURE__ */ new Date()) {
602
739
  const peers = loadPeers(dir);
603
740
  const at = now.toISOString();
604
- 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
+ };
605
749
  const existing = peers.findIndex((p) => p.handle === clean.handle);
606
750
  let isNew;
607
751
  let peer;
608
752
  if (existing >= 0) {
609
753
  isNew = false;
610
- 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
+ };
611
762
  peers[existing] = peer;
612
763
  } else {
613
764
  isNew = true;
614
765
  peer = { ...clean, firstSeenAt: at, lastSeenAt: at };
615
766
  peers.push(peer);
616
767
  }
617
- mkdirSync2(dir, { recursive: true });
618
- 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");
619
770
  return { peer, isNew };
620
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
+ }
621
785
  async function startDiscovery(opts) {
622
786
  const { hello, stateDir = defaultStateDir(), onPeer, onLink, notify = vibeCoreNotify } = opts;
623
787
  const isBlocked2 = opts.isBlocked;
@@ -630,7 +794,16 @@ async function startDiscovery(opts) {
630
794
  swarm.on("connection", (socket, info) => {
631
795
  const remoteKey = info.publicKey.toString("hex");
632
796
  socket.write(
633
- 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"
634
807
  );
635
808
  let buf = "";
636
809
  let handedOff = false;
@@ -645,11 +818,18 @@ async function startDiscovery(opts) {
645
818
  const frame = parseFrame(line);
646
819
  if (frame === null) continue;
647
820
  if (frame.t !== "hello") continue;
821
+ const verdict = classifyHelloIdentity(frame);
822
+ if (verdict === "drop") continue;
648
823
  const peer = {
649
824
  handle: frame.handle,
650
825
  league: frame.league,
651
- 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 } : {}
652
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
+ }
653
833
  if (!acceptLeague(peer.league)) continue;
654
834
  if (isBlocked2 !== void 0 && isBlocked2(peer.handle)) continue;
655
835
  peers.set(remoteKey, peer);
@@ -672,6 +852,9 @@ async function startDiscovery(opts) {
672
852
  socket.off("data", onData);
673
853
  if (onLink !== void 0) {
674
854
  const link = createPeerLink(socket, peer, buf);
855
+ link.onMessage(() => {
856
+ recordPeerMessage(peer.handle, stateDir);
857
+ });
675
858
  onLink(link);
676
859
  }
677
860
  buf = "";
@@ -757,23 +940,24 @@ function leaguesWithin(name, width) {
757
940
  var TOKENS_ENV = "VIBEDATING_TOKENS";
758
941
  var DEMO_TOTAL_TOKENS = 234e5;
759
942
  var MS_PER_DAY = 864e5;
760
- async function readUsage(harness = "claude-code") {
761
- const verified = await tryReadVerifiedUsage(harness);
762
- if (verified) return verified;
763
- const injected = parseTokensEnv(process.env[TOKENS_ENV]);
764
- 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);
765
950
  const now = /* @__PURE__ */ new Date();
766
951
  return {
767
952
  harness,
768
- totalTokens,
769
- 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 } : {},
770
957
  windowStart: new Date(now.getTime() - 30 * MS_PER_DAY).toISOString(),
771
958
  windowEnd: now.toISOString()
772
959
  };
773
960
  }
774
- async function tryReadVerifiedUsage(_harness) {
775
- return null;
776
- }
777
961
  var TOKEN_MULT = {
778
962
  "": 1,
779
963
  k: 1e3,
@@ -864,6 +1048,11 @@ export {
864
1048
  isBlocked,
865
1049
  addBlock,
866
1050
  removeBlock,
1051
+ loadOrCreateIdentity,
1052
+ canonicalHelloClaims,
1053
+ signHelloClaims,
1054
+ verifyHelloClaims,
1055
+ classifyHelloIdentity,
867
1056
  TOPIC_PREFIX,
868
1057
  leagueTopic,
869
1058
  LIVE_NOTICE,
@@ -871,6 +1060,7 @@ export {
871
1060
  parseHandshake,
872
1061
  loadPeers,
873
1062
  recordPeer,
1063
+ recordPeerMessage,
874
1064
  startDiscovery,
875
1065
  LEAGUES,
876
1066
  BELOW_LEAGUE,
@@ -881,7 +1071,6 @@ export {
881
1071
  TOKENS_ENV,
882
1072
  DEMO_TOTAL_TOKENS,
883
1073
  readUsage,
884
- tryReadVerifiedUsage,
885
1074
  parseTokensEnv,
886
1075
  CANDIDATES,
887
1076
  matches,