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.
- package/README.md +16 -10
- package/dist/{chunk-3VT4EOBX.js → chunk-43IYNWES.js} +401 -212
- package/dist/{chunk-OVO2CQ7X.js → chunk-ZB56ZBAR.js} +2 -2
- package/dist/cli.js +71 -38
- package/dist/index.d.ts +130 -31
- package/dist/index.js +13 -3
- package/dist/mcp.js +2 -2
- package/package.json +2 -2
|
@@ -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,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
|
|
727
|
+
return path4.join(dir, "peers.json");
|
|
591
728
|
}
|
|
592
729
|
function loadPeers(dir = defaultStateDir()) {
|
|
593
730
|
try {
|
|
594
|
-
const raw =
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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({
|
|
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
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
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:
|
|
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,
|