vibedate 0.1.1 → 0.2.1

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.
@@ -0,0 +1,744 @@
1
+ // src/index.ts
2
+ import { createConsentLedger as createConsentLedger2 } from "@pooriaarab/vibe-core";
3
+
4
+ // 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";
8
+ import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
9
+
10
+ // src/frame.ts
11
+ var MAX_TEXT_LEN = 4e3;
12
+ var MAX_ID_LEN = 64;
13
+ var MAX_B64_CHUNK_LEN = 16 * 1024;
14
+ var MAX_MEDIA_SIZE = 25 * 1024 * 1024;
15
+ var MAX_MIME_LEN = 128;
16
+ var MAX_NAME_LEN = 256;
17
+ var MAX_SDP_LEN = 64 * 1024;
18
+ var MAX_CANDIDATE_LEN = 4 * 1024;
19
+ var MAX_FRAME_LEN = Math.max(MAX_B64_CHUNK_LEN, 2 * MAX_SDP_LEN) + 2048;
20
+ function serializeFrame(f) {
21
+ return JSON.stringify(f);
22
+ }
23
+ function parseFrame(raw) {
24
+ const text = typeof raw === "string" ? raw : raw.toString("utf8");
25
+ if (text.length > MAX_FRAME_LEN) return null;
26
+ let d;
27
+ try {
28
+ d = JSON.parse(text);
29
+ } catch {
30
+ return null;
31
+ }
32
+ if (typeof d !== "object" || d === null || Array.isArray(d)) return null;
33
+ const r = d;
34
+ switch (r["t"]) {
35
+ case "bye":
36
+ return { t: "bye" };
37
+ case "typing":
38
+ return { t: "typing" };
39
+ case "hello": {
40
+ const { handle, league: league2, harness } = r;
41
+ if (typeof handle !== "string" || typeof league2 !== "string") return null;
42
+ return { t: "hello", handle, league: league2, harness: typeof harness === "string" ? harness : "unknown" };
43
+ }
44
+ case "msg": {
45
+ const id = r["id"];
46
+ const txt = r["text"];
47
+ const at = r["at"];
48
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LEN) return null;
49
+ if (typeof txt !== "string" || txt.length === 0 || txt.length > MAX_TEXT_LEN) return null;
50
+ if (typeof at !== "number" || !Number.isFinite(at)) return null;
51
+ return { t: "msg", id, text: txt, at };
52
+ }
53
+ case "media-start": {
54
+ const id = r["id"];
55
+ const mime = r["mime"];
56
+ const size = r["size"];
57
+ const name = r["name"];
58
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LEN) return null;
59
+ if (typeof mime !== "string" || mime.length > MAX_MIME_LEN) return null;
60
+ if (typeof name !== "string" || name.length > MAX_NAME_LEN) return null;
61
+ if (typeof size !== "number" || !Number.isFinite(size) || !Number.isInteger(size) || size < 0 || size > MAX_MEDIA_SIZE)
62
+ return null;
63
+ return { t: "media-start", id, mime, size, name };
64
+ }
65
+ case "media-chunk": {
66
+ const id = r["id"];
67
+ const seq = r["seq"];
68
+ const b64 = r["b64"];
69
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LEN) return null;
70
+ if (typeof seq !== "number" || !Number.isFinite(seq) || !Number.isInteger(seq) || seq < 0)
71
+ return null;
72
+ if (typeof b64 !== "string" || b64.length === 0 || b64.length > MAX_B64_CHUNK_LEN) return null;
73
+ return { t: "media-chunk", id, seq, b64 };
74
+ }
75
+ case "media-end": {
76
+ const id = r["id"];
77
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LEN) return null;
78
+ return { t: "media-end", id };
79
+ }
80
+ case "rtc-offer": {
81
+ const sdp = r["sdp"];
82
+ if (typeof sdp !== "string" || sdp.length === 0 || sdp.length > MAX_SDP_LEN) return null;
83
+ return { t: "rtc-offer", sdp };
84
+ }
85
+ case "rtc-answer": {
86
+ const sdp = r["sdp"];
87
+ if (typeof sdp !== "string" || sdp.length === 0 || sdp.length > MAX_SDP_LEN) return null;
88
+ return { t: "rtc-answer", sdp };
89
+ }
90
+ case "rtc-ice": {
91
+ const candidate = r["candidate"];
92
+ if (typeof candidate !== "string" || candidate.length > MAX_CANDIDATE_LEN) return null;
93
+ return { t: "rtc-ice", candidate };
94
+ }
95
+ default:
96
+ return null;
97
+ }
98
+ }
99
+
100
+ // src/link.ts
101
+ import { randomUUID as randomUUID2 } from "crypto";
102
+
103
+ // src/media.ts
104
+ import { randomUUID } from "crypto";
105
+ import { readFile } from "fs/promises";
106
+ import { writeFileSync } from "fs";
107
+ import os from "os";
108
+ import path from "path";
109
+ var DEFAULT_CHUNK_BYTES = Math.floor(MAX_B64_CHUNK_LEN * 3 / 4);
110
+ var MIME_BY_EXT = {
111
+ ".png": "image/png",
112
+ ".jpg": "image/jpeg",
113
+ ".jpeg": "image/jpeg",
114
+ ".gif": "image/gif",
115
+ ".webp": "image/webp",
116
+ ".mp4": "video/mp4",
117
+ ".webm": "video/webm"
118
+ };
119
+ function inferMime(name) {
120
+ return MIME_BY_EXT[path.extname(name).toLowerCase()] ?? "application/octet-stream";
121
+ }
122
+ function writeFrame(socket, line) {
123
+ return new Promise((resolve) => {
124
+ const ok = socket.write(line);
125
+ if (ok) resolve();
126
+ else socket.once("drain", () => resolve());
127
+ });
128
+ }
129
+ async function sendMedia(opts) {
130
+ const { socket, data, mime, name } = opts;
131
+ const id = opts.id ?? randomUUID();
132
+ const size = data.length;
133
+ if (size > MAX_MEDIA_SIZE) {
134
+ throw new Error(`media too large: ${size} bytes exceeds ${MAX_MEDIA_SIZE} byte cap`);
135
+ }
136
+ if (mime.length > MAX_MIME_LEN) throw new Error(`mime too long: ${mime.length} > ${MAX_MIME_LEN}`);
137
+ if (name.length > MAX_NAME_LEN) throw new Error(`name too long: ${name.length} > ${MAX_NAME_LEN}`);
138
+ await writeFrame(socket, serializeFrame({ t: "media-start", id, mime, size, name }) + "\n");
139
+ const chunkBytes = opts.chunkBytes ?? DEFAULT_CHUNK_BYTES;
140
+ let seq = 0;
141
+ for (let off = 0; off < size; off += chunkBytes) {
142
+ const b64 = data.subarray(off, off + chunkBytes).toString("base64");
143
+ await writeFrame(socket, serializeFrame({ t: "media-chunk", id, seq, b64 }) + "\n");
144
+ seq++;
145
+ }
146
+ await writeFrame(socket, serializeFrame({ t: "media-end", id }) + "\n");
147
+ return { id, size };
148
+ }
149
+ async function sendMediaFile(opts) {
150
+ const data = await readFile(opts.path);
151
+ const name = opts.name ?? path.basename(opts.path);
152
+ const mime = opts.mime ?? inferMime(name);
153
+ return sendMedia({
154
+ socket: opts.socket,
155
+ data,
156
+ mime,
157
+ name,
158
+ id: opts.id,
159
+ chunkBytes: opts.chunkBytes
160
+ });
161
+ }
162
+ function safeName(id, name) {
163
+ const ext = path.extname(name);
164
+ return ext && /^\.[A-Za-z0-9]{1,16}$/.test(ext) ? `${id}${ext}` : id;
165
+ }
166
+ var MediaReceiver = class {
167
+ constructor(onMedia, opts = {}) {
168
+ this.onMedia = onMedia;
169
+ this.opts = opts;
170
+ }
171
+ onMedia;
172
+ opts;
173
+ transfers = /* @__PURE__ */ new Map();
174
+ /** Abort a transfer (drop all state for `id`) without delivering. */
175
+ abort(id) {
176
+ this.transfers.delete(id);
177
+ }
178
+ /** Feed one parsed media frame. Never throws. */
179
+ handle(frame) {
180
+ switch (frame.t) {
181
+ case "media-start": {
182
+ this.transfers.set(frame.id, {
183
+ mime: frame.mime,
184
+ name: frame.name,
185
+ size: frame.size,
186
+ nextSeq: 0,
187
+ chunks: [],
188
+ received: 0
189
+ });
190
+ return;
191
+ }
192
+ case "media-chunk": {
193
+ const tx = this.transfers.get(frame.id);
194
+ if (!tx) return;
195
+ if (frame.seq !== tx.nextSeq) {
196
+ this.abort(frame.id);
197
+ return;
198
+ }
199
+ let bytes;
200
+ try {
201
+ bytes = Buffer.from(frame.b64, "base64");
202
+ } catch {
203
+ this.abort(frame.id);
204
+ return;
205
+ }
206
+ const received = tx.received + bytes.length;
207
+ if (received > tx.size || received > MAX_MEDIA_SIZE) {
208
+ this.abort(frame.id);
209
+ return;
210
+ }
211
+ tx.chunks.push(bytes);
212
+ tx.received = received;
213
+ tx.nextSeq += 1;
214
+ return;
215
+ }
216
+ case "media-end": {
217
+ const tx = this.transfers.get(frame.id);
218
+ if (!tx) return;
219
+ this.transfers.delete(frame.id);
220
+ if (tx.received !== tx.size) return;
221
+ const buf = Buffer.concat(tx.chunks, tx.received);
222
+ const filePath = path.join(this.opts.tmpDir ?? os.tmpdir(), safeName(frame.id, tx.name));
223
+ try {
224
+ writeFileSync(filePath, buf);
225
+ } catch {
226
+ return;
227
+ }
228
+ this.onMedia({ mime: tx.mime, name: tx.name, path: filePath, size: tx.received });
229
+ return;
230
+ }
231
+ default:
232
+ return;
233
+ }
234
+ }
235
+ };
236
+
237
+ // src/link.ts
238
+ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
239
+ const messageCbs = /* @__PURE__ */ new Set();
240
+ const mediaCbs = /* @__PURE__ */ new Set();
241
+ const signalCbs = /* @__PURE__ */ new Set();
242
+ const closeCbs = /* @__PURE__ */ new Set();
243
+ let buf = initialBuffer;
244
+ let closed = false;
245
+ let mediaReceiver;
246
+ const ensureMediaReceiver = () => {
247
+ if (!mediaReceiver) {
248
+ mediaReceiver = new MediaReceiver(
249
+ (m) => {
250
+ for (const cb of mediaCbs) cb(m);
251
+ },
252
+ { tmpDir: linkOpts.mediaTmpDir }
253
+ );
254
+ }
255
+ return mediaReceiver;
256
+ };
257
+ const dispatch = (frame) => {
258
+ switch (frame.t) {
259
+ case "msg": {
260
+ const m = { id: frame.id, text: frame.text, at: frame.at };
261
+ for (const cb of messageCbs) cb(m);
262
+ break;
263
+ }
264
+ case "media-start":
265
+ case "media-chunk":
266
+ case "media-end": {
267
+ mediaReceiver?.handle(frame);
268
+ break;
269
+ }
270
+ case "rtc-offer":
271
+ case "rtc-answer":
272
+ case "rtc-ice": {
273
+ const f = frame;
274
+ for (const cb of signalCbs) cb(f);
275
+ break;
276
+ }
277
+ case "bye": {
278
+ if (!closed) {
279
+ closed = true;
280
+ for (const cb of closeCbs) cb();
281
+ }
282
+ break;
283
+ }
284
+ // 'hello' / 'typing' have no meaning at the link layer — hello already
285
+ // happened; typing is a future affordance. Ignore silently.
286
+ default:
287
+ break;
288
+ }
289
+ };
290
+ const pump = () => {
291
+ let nl;
292
+ while ((nl = buf.indexOf("\n")) >= 0) {
293
+ const line = buf.slice(0, nl);
294
+ buf = buf.slice(nl + 1);
295
+ if (line.trim() === "") continue;
296
+ const frame = parseFrame(line);
297
+ if (frame === null) continue;
298
+ dispatch(frame);
299
+ }
300
+ };
301
+ pump();
302
+ socket.on("data", (chunk) => {
303
+ buf += chunk.toString("utf8");
304
+ pump();
305
+ });
306
+ socket.on("end", () => {
307
+ if (!closed) {
308
+ closed = true;
309
+ for (const cb of closeCbs) cb();
310
+ }
311
+ });
312
+ socket.on("close", () => {
313
+ if (!closed) {
314
+ closed = true;
315
+ for (const cb of closeCbs) cb();
316
+ }
317
+ });
318
+ socket.on("error", () => {
319
+ if (!closed) {
320
+ closed = true;
321
+ for (const cb of closeCbs) cb();
322
+ }
323
+ });
324
+ return {
325
+ hello,
326
+ send(text) {
327
+ if (closed) return;
328
+ const frame = { t: "msg", id: randomUUID2(), text, at: Date.now() };
329
+ socket.write(serializeFrame(frame) + "\n");
330
+ },
331
+ async sendMedia(filePath, opts = {}) {
332
+ if (closed) return { id: "", size: 0 };
333
+ return sendMediaFile({ socket, path: filePath, mime: opts.mime, name: opts.name });
334
+ },
335
+ sendSignal(frame) {
336
+ if (closed) return;
337
+ socket.write(serializeFrame(frame) + "\n");
338
+ },
339
+ onMessage(cb) {
340
+ messageCbs.add(cb);
341
+ },
342
+ onMedia(cb) {
343
+ ensureMediaReceiver();
344
+ mediaCbs.add(cb);
345
+ },
346
+ onSignal(cb) {
347
+ signalCbs.add(cb);
348
+ },
349
+ onClose(cb) {
350
+ closeCbs.add(cb);
351
+ },
352
+ close() {
353
+ if (closed) return;
354
+ closed = true;
355
+ try {
356
+ socket.write(serializeFrame({ t: "bye" }) + "\n");
357
+ } catch {
358
+ }
359
+ try {
360
+ socket.end();
361
+ } catch {
362
+ }
363
+ }
364
+ };
365
+ }
366
+
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
+ // src/p2p.ts
437
+ var TOPIC_PREFIX = "vibedate:";
438
+ function leagueTopic(leagueName) {
439
+ return createHash("sha256").update(`${TOPIC_PREFIX}${leagueName}`, "utf8").digest();
440
+ }
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;
443
+ var MAX_LEAGUE_LEN = 32;
444
+ var MAX_HARNESS_LEN = 64;
445
+ var MAX_HANDSHAKE_LEN = 4096;
446
+ var REFRESH_INTERVAL_MS = 5e3;
447
+ function serializeHandshake(hello) {
448
+ return JSON.stringify({
449
+ handle: hello.handle,
450
+ league: hello.league,
451
+ harness: hello.harness
452
+ });
453
+ }
454
+ function parseHandshake(raw) {
455
+ const text = typeof raw === "string" ? raw : raw.toString("utf8");
456
+ if (text.length > MAX_HANDSHAKE_LEN) return null;
457
+ let data;
458
+ try {
459
+ data = JSON.parse(text);
460
+ } catch {
461
+ return null;
462
+ }
463
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
464
+ const rec = data;
465
+ const handle = rec["handle"];
466
+ const league2 = rec["league"];
467
+ if (typeof handle !== "string" || handle.length === 0 || handle.length > MAX_HANDLE_LEN) {
468
+ return null;
469
+ }
470
+ if (typeof league2 !== "string" || league2.length === 0 || league2.length > MAX_LEAGUE_LEN) {
471
+ return null;
472
+ }
473
+ const harness = rec["harness"];
474
+ return {
475
+ handle,
476
+ league: league2,
477
+ harness: typeof harness === "string" && harness.length > 0 && harness.length <= MAX_HARNESS_LEN ? harness : "unknown"
478
+ };
479
+ }
480
+ function peersPath(dir) {
481
+ return path3.join(dir, "peers.json");
482
+ }
483
+ function loadPeers(dir = defaultStateDir()) {
484
+ try {
485
+ const raw = readFileSync2(peersPath(dir), "utf8");
486
+ const data = JSON.parse(raw);
487
+ return Array.isArray(data.peers) ? data.peers : [];
488
+ } catch {
489
+ return [];
490
+ }
491
+ }
492
+ function recordPeer(hello, dir = defaultStateDir(), now = /* @__PURE__ */ new Date()) {
493
+ const peers = loadPeers(dir);
494
+ const at = now.toISOString();
495
+ const clean = { handle: hello.handle, league: hello.league, harness: hello.harness };
496
+ const existing = peers.findIndex((p) => p.handle === clean.handle);
497
+ let isNew;
498
+ let peer;
499
+ if (existing >= 0) {
500
+ isNew = false;
501
+ peer = { ...peers[existing], ...clean, lastSeenAt: at };
502
+ peers[existing] = peer;
503
+ } else {
504
+ isNew = true;
505
+ peer = { ...clean, firstSeenAt: at, lastSeenAt: at };
506
+ peers.push(peer);
507
+ }
508
+ mkdirSync2(dir, { recursive: true });
509
+ writeFileSync3(peersPath(dir), JSON.stringify({ peers }, null, 2) + "\n", "utf8");
510
+ return { peer, isNew };
511
+ }
512
+ async function startDiscovery(opts) {
513
+ const { hello, stateDir = defaultStateDir(), onPeer, onLink, notify = vibeCoreNotify } = opts;
514
+ const topic = opts.topic ?? leagueTopic(hello.league);
515
+ const { default: Hyperswarm } = await import("hyperswarm");
516
+ const swarm = new Hyperswarm(opts.bootstrap === void 0 ? {} : { bootstrap: opts.bootstrap });
517
+ await swarm.dht.fullyBootstrapped();
518
+ const peers = /* @__PURE__ */ new Map();
519
+ swarm.on("connection", (socket, info) => {
520
+ const remoteKey = info.publicKey.toString("hex");
521
+ socket.write(
522
+ serializeFrame({ t: "hello", handle: hello.handle, league: hello.league, harness: hello.harness }) + "\n"
523
+ );
524
+ let buf = "";
525
+ let handedOff = false;
526
+ const onData = (chunk) => {
527
+ if (handedOff) return;
528
+ buf += chunk.toString("utf8");
529
+ let nl;
530
+ while ((nl = buf.indexOf("\n")) >= 0) {
531
+ const line = buf.slice(0, nl);
532
+ buf = buf.slice(nl + 1);
533
+ if (line.trim() === "") continue;
534
+ const frame = parseFrame(line);
535
+ if (frame === null) continue;
536
+ if (frame.t !== "hello") continue;
537
+ const peer = {
538
+ handle: frame.handle,
539
+ league: frame.league,
540
+ harness: frame.harness
541
+ };
542
+ if (peer.league !== hello.league) continue;
543
+ peers.set(remoteKey, peer);
544
+ const { isNew } = recordPeer(peer, stateDir);
545
+ if (isNew) {
546
+ try {
547
+ notify(
548
+ makeEvent("match", hello.harness, process.cwd(), {
549
+ summary: `matched with ${peer.handle} - LIVE SAME LEAGUE`,
550
+ handle: peer.handle,
551
+ league: peer.league,
552
+ harness: peer.harness
553
+ })
554
+ );
555
+ } catch {
556
+ }
557
+ }
558
+ onPeer?.(peer, isNew);
559
+ handedOff = true;
560
+ socket.off("data", onData);
561
+ if (onLink !== void 0) {
562
+ const link = createPeerLink(socket, peer, buf);
563
+ onLink(link);
564
+ }
565
+ buf = "";
566
+ return;
567
+ }
568
+ };
569
+ socket.on("data", onData);
570
+ socket.on("error", () => {
571
+ });
572
+ });
573
+ const discovery = swarm.join(topic, { server: true, client: true });
574
+ const ready = discovery.flushed().catch(() => void 0);
575
+ await ready;
576
+ const refresher = setInterval(() => {
577
+ void discovery.refresh({ server: true, client: true }).catch(() => {
578
+ });
579
+ }, REFRESH_INTERVAL_MS);
580
+ refresher.unref();
581
+ let closed = false;
582
+ return {
583
+ topic,
584
+ hello,
585
+ peers,
586
+ ready,
587
+ async close() {
588
+ if (closed) return;
589
+ closed = true;
590
+ clearInterval(refresher);
591
+ try {
592
+ await swarm.leave(topic);
593
+ } catch {
594
+ }
595
+ await swarm.destroy();
596
+ }
597
+ };
598
+ }
599
+
600
+ // src/index.ts
601
+ var LEAGUES = [
602
+ { name: "1M", min: 1e6, max: 4999999 },
603
+ { name: "5M", min: 5e6, max: 9999999 },
604
+ { name: "10M", min: 1e7, max: 99999999 },
605
+ { name: "100M", min: 1e8, max: 999999999 },
606
+ { name: "1B+", min: 1e9, max: Number.POSITIVE_INFINITY }
607
+ ];
608
+ var BELOW_LEAGUE = "below-1M";
609
+ function league(totalTokens) {
610
+ const n = Math.max(0, Math.floor(totalTokens));
611
+ for (const l of LEAGUES) {
612
+ if (n >= l.min && n <= l.max) {
613
+ return { name: l.name, min: l.min };
614
+ }
615
+ }
616
+ return { name: BELOW_LEAGUE, min: 0 };
617
+ }
618
+ function leagueIndex(name) {
619
+ if (name === BELOW_LEAGUE) return -1;
620
+ return LEAGUES.findIndex((l) => l.name === name);
621
+ }
622
+ var TOKENS_ENV = "VIBEDATING_TOKENS";
623
+ var DEMO_TOTAL_TOKENS = 234e5;
624
+ var MS_PER_DAY = 864e5;
625
+ async function readUsage(harness = "claude-code") {
626
+ const verified = await tryReadVerifiedUsage(harness);
627
+ if (verified) return verified;
628
+ const injected = parseTokensEnv(process.env[TOKENS_ENV]);
629
+ const totalTokens = injected ?? DEMO_TOTAL_TOKENS;
630
+ const now = /* @__PURE__ */ new Date();
631
+ return {
632
+ harness,
633
+ totalTokens,
634
+ verified: false,
635
+ windowStart: new Date(now.getTime() - 30 * MS_PER_DAY).toISOString(),
636
+ windowEnd: now.toISOString()
637
+ };
638
+ }
639
+ async function tryReadVerifiedUsage(_harness) {
640
+ return null;
641
+ }
642
+ var TOKEN_MULT = {
643
+ "": 1,
644
+ k: 1e3,
645
+ K: 1e3,
646
+ m: 1e6,
647
+ M: 1e6,
648
+ b: 1e9,
649
+ B: 1e9
650
+ };
651
+ function parseTokensEnv(raw) {
652
+ if (raw === void 0) return void 0;
653
+ const trimmed = raw.trim();
654
+ if (trimmed === "") return void 0;
655
+ const match = /^([0-9]*\.?[0-9]+)\s*([kKmMbB]?)$/.exec(trimmed);
656
+ if (!match) return void 0;
657
+ const num = Number(match[1]);
658
+ if (!Number.isFinite(num) || num < 0) return void 0;
659
+ const mult = TOKEN_MULT[match[2] ?? ""] ?? 1;
660
+ return Math.floor(num * mult);
661
+ }
662
+ var CANDIDATES = [
663
+ {
664
+ handle: "@merge_conflict_therapist",
665
+ league: "10M",
666
+ bio: ["Resolves conflicts for a living \u2014 code and otherwise.", "Currently: 47 tabs open, 3 are Stack Overflow."]
667
+ },
668
+ {
669
+ handle: "@rebase_romantic",
670
+ league: "5M",
671
+ bio: ["Rewrites history for a living, git and otherwise.", "Looking for someone who squashes commits and grudges."]
672
+ },
673
+ {
674
+ handle: "@0xInsomniac",
675
+ league: "1B+",
676
+ bio: ["Token count is classified. Ask my therapist.", "Has never once respected a rate limit."]
677
+ },
678
+ {
679
+ handle: "@yolo_to_main",
680
+ league: "1M",
681
+ bio: ["No branches, no regrets, no CI.", "A green checkmark is a state of mind."]
682
+ },
683
+ {
684
+ handle: "@async_awaits_you",
685
+ league: "10M",
686
+ bio: ["Promises kept, unlike my sleep schedule.", "DMs are non-blocking. Replies eventually resolve."]
687
+ },
688
+ {
689
+ handle: "@nullish_and_void",
690
+ league: "5M",
691
+ bio: ["Coalescing since 2019.", "My love language is optional chaining."]
692
+ },
693
+ {
694
+ handle: "@ctrl_z_daddy",
695
+ league: "100M",
696
+ bio: ["Undo is my safe word.", "Refactors everything, including this bio, twice."]
697
+ },
698
+ {
699
+ handle: "@segfault_sonnet",
700
+ league: "1M",
701
+ bio: ["Writes poetry in stack traces.", "Core dumped. Heart, mostly, open."]
702
+ },
703
+ {
704
+ handle: "@the_lint_whisperer",
705
+ league: "10M",
706
+ bio: ["Zero warnings, zero regrets, zero chill.", "Will fix your semicolons without being asked."]
707
+ }
708
+ ];
709
+ function matches(myLeague, candidates = CANDIDATES) {
710
+ const myIdx = leagueIndex(myLeague);
711
+ return candidates.filter((c) => {
712
+ const idx = leagueIndex(c.league);
713
+ if (idx < 0) return false;
714
+ return Math.abs(idx - myIdx) <= 1;
715
+ });
716
+ }
717
+
718
+ export {
719
+ parseFrame,
720
+ connectProfile,
721
+ loadProfile,
722
+ grantLiveConsent,
723
+ canShareLive,
724
+ TOPIC_PREFIX,
725
+ leagueTopic,
726
+ LIVE_NOTICE,
727
+ serializeHandshake,
728
+ parseHandshake,
729
+ loadPeers,
730
+ recordPeer,
731
+ startDiscovery,
732
+ LEAGUES,
733
+ BELOW_LEAGUE,
734
+ league,
735
+ leagueIndex,
736
+ TOKENS_ENV,
737
+ DEMO_TOTAL_TOKENS,
738
+ readUsage,
739
+ tryReadVerifiedUsage,
740
+ parseTokensEnv,
741
+ CANDIDATES,
742
+ matches,
743
+ createConsentLedger2 as createConsentLedger
744
+ };