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/README.md +16 -10
- package/dist/{chunk-VBLFAOV3.js → chunk-43IYNWES.js} +413 -105
- package/dist/{chunk-DMA7MSN7.js → chunk-ZB56ZBAR.js} +2 -2
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +310 -40
- package/dist/index.d.ts +137 -31
- package/dist/index.js +13 -3
- package/dist/mcp.js +2 -2
- package/package.json +4 -3
|
@@ -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
|
|
7
|
-
import
|
|
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
|
-
|
|
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
|
|
108
|
-
import
|
|
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[
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
514
|
+
const filePath = path3.join(this.opts.tmpDir ?? os2.tmpdir(), safeName(frame.id, tx.name));
|
|
223
515
|
try {
|
|
224
|
-
|
|
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
|
|
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 >
|
|
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
|
|
727
|
+
return path4.join(dir, "peers.json");
|
|
482
728
|
}
|
|
483
729
|
function loadPeers(dir = defaultStateDir()) {
|
|
484
730
|
try {
|
|
485
|
-
const raw =
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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({
|
|
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
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
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:
|
|
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,
|