vibedate 0.5.0 → 0.6.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/dist/{chunk-I5U3O4RH.js → chunk-JIBC3OHV.js} +1 -1
- package/dist/{chunk-KKWP4DLY.js → chunk-VZXPYU2C.js} +243 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +120 -12
- package/dist/index.d.ts +136 -1
- package/dist/index.js +15 -1
- package/dist/mcp.js +2 -2
- package/package.json +3 -2
|
@@ -388,6 +388,44 @@ function classifyHelloIdentity(hello) {
|
|
|
388
388
|
if (hello.nonce === void 0 || hello.sig === void 0) return "drop";
|
|
389
389
|
return verifyHelloClaims(hello, { pubkey: hello.pubkey, nonce: hello.nonce, sig: hello.sig }) ? "verified" : "drop";
|
|
390
390
|
}
|
|
391
|
+
var NOSTR_FILE = "nostr.json";
|
|
392
|
+
function nostrPath(dir) {
|
|
393
|
+
return path2.join(dir, NOSTR_FILE);
|
|
394
|
+
}
|
|
395
|
+
function isStoredNostrKey(data) {
|
|
396
|
+
if (typeof data !== "object" || data === null) return false;
|
|
397
|
+
const r = data;
|
|
398
|
+
return typeof r["sk"] === "string" && /^[0-9a-f]{64}$/.test(r["sk"]) && typeof r["pubkey"] === "string" && /^[0-9a-f]{64}$/.test(r["pubkey"]) && typeof r["createdAt"] === "string";
|
|
399
|
+
}
|
|
400
|
+
async function loadOrCreateNostrKey(dir = defaultStateDir()) {
|
|
401
|
+
try {
|
|
402
|
+
const raw = readFileSync2(nostrPath(dir), "utf8");
|
|
403
|
+
const data = JSON.parse(raw);
|
|
404
|
+
if (isStoredNostrKey(data)) {
|
|
405
|
+
try {
|
|
406
|
+
chmodSync(nostrPath(dir), 384);
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
return { sk: Buffer.from(data.sk, "hex"), pubkey: data.pubkey };
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
const { generateSecretKey, getPublicKey } = await import("nostr-tools");
|
|
414
|
+
const sk = generateSecretKey();
|
|
415
|
+
const pubkey = getPublicKey(sk);
|
|
416
|
+
const skHex = Buffer.from(sk).toString("hex");
|
|
417
|
+
mkdirSync2(dir, { recursive: true });
|
|
418
|
+
writeFileSync2(
|
|
419
|
+
nostrPath(dir),
|
|
420
|
+
JSON.stringify({ sk: skHex, pubkey, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
421
|
+
{ encoding: "utf8", mode: 384 }
|
|
422
|
+
);
|
|
423
|
+
try {
|
|
424
|
+
chmodSync(nostrPath(dir), 384);
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
return { sk, pubkey };
|
|
428
|
+
}
|
|
391
429
|
|
|
392
430
|
// src/link.ts
|
|
393
431
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -909,6 +947,204 @@ async function startDiscovery(opts) {
|
|
|
909
947
|
};
|
|
910
948
|
}
|
|
911
949
|
|
|
950
|
+
// src/relay.ts
|
|
951
|
+
import { createHash as createHash2 } from "crypto";
|
|
952
|
+
var DEFAULT_RELAYS = [
|
|
953
|
+
"wss://relay.damus.io",
|
|
954
|
+
"wss://nostr.mom",
|
|
955
|
+
"wss://relay.snort.social"
|
|
956
|
+
];
|
|
957
|
+
var VIBEDATE_MESSAGE_KIND = 4;
|
|
958
|
+
var VIBEDATE_PRESENCE_KIND = 30078;
|
|
959
|
+
var PRESENCE_D_TAG = "vibedating";
|
|
960
|
+
var PRESENCE_MARKER = JSON.stringify({ app: "vibedating", v: 1 });
|
|
961
|
+
var ED25519_TAG = "ed25519";
|
|
962
|
+
function conversationTag(myEd25519Hex, peerEd25519Hex) {
|
|
963
|
+
const lo = myEd25519Hex < peerEd25519Hex ? myEd25519Hex : peerEd25519Hex;
|
|
964
|
+
const hi = myEd25519Hex < peerEd25519Hex ? peerEd25519Hex : myEd25519Hex;
|
|
965
|
+
return createHash2("sha256").update(`vibedate:nostr:${lo}:${hi}`, "utf8").digest("hex");
|
|
966
|
+
}
|
|
967
|
+
var nostrPromise;
|
|
968
|
+
function nostr() {
|
|
969
|
+
if (!nostrPromise) nostrPromise = import("nostr-tools");
|
|
970
|
+
return nostrPromise;
|
|
971
|
+
}
|
|
972
|
+
function tagValue(event, name) {
|
|
973
|
+
for (const tag of event.tags) {
|
|
974
|
+
if (tag[0] === name && typeof tag[1] === "string") return tag[1];
|
|
975
|
+
}
|
|
976
|
+
return void 0;
|
|
977
|
+
}
|
|
978
|
+
async function createNostrRelayLink(opts) {
|
|
979
|
+
const { finalizeEvent, nip04 } = await nostr();
|
|
980
|
+
const { myNostr, myEd25519Hex, peerEd25519Hex, hello, transport } = opts;
|
|
981
|
+
const convTag = conversationTag(myEd25519Hex, peerEd25519Hex);
|
|
982
|
+
const messageCbs = /* @__PURE__ */ new Set();
|
|
983
|
+
const closeCbs = /* @__PURE__ */ new Set();
|
|
984
|
+
const mediaCbs = /* @__PURE__ */ new Set();
|
|
985
|
+
const signalCbs = /* @__PURE__ */ new Set();
|
|
986
|
+
let closed = false;
|
|
987
|
+
let peerNostrPubkey;
|
|
988
|
+
const pending = [];
|
|
989
|
+
const publish = (template) => {
|
|
990
|
+
let event;
|
|
991
|
+
try {
|
|
992
|
+
event = finalizeEvent(
|
|
993
|
+
{ ...template, created_at: Math.floor(Date.now() / 1e3) },
|
|
994
|
+
myNostr.sk
|
|
995
|
+
);
|
|
996
|
+
} catch {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
transport.publish(event);
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
const publishPresence = () => {
|
|
1005
|
+
publish({
|
|
1006
|
+
kind: VIBEDATE_PRESENCE_KIND,
|
|
1007
|
+
content: PRESENCE_MARKER,
|
|
1008
|
+
tags: [
|
|
1009
|
+
["d", PRESENCE_D_TAG],
|
|
1010
|
+
["t", convTag],
|
|
1011
|
+
[ED25519_TAG, myEd25519Hex]
|
|
1012
|
+
]
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
const sendEncrypted = (text, recipientPubkey) => {
|
|
1016
|
+
let ciphertext;
|
|
1017
|
+
try {
|
|
1018
|
+
ciphertext = nip04.encrypt(myNostr.sk, recipientPubkey, text);
|
|
1019
|
+
} catch {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
publish({
|
|
1023
|
+
kind: VIBEDATE_MESSAGE_KIND,
|
|
1024
|
+
content: ciphertext,
|
|
1025
|
+
tags: [
|
|
1026
|
+
["t", convTag],
|
|
1027
|
+
["p", recipientPubkey],
|
|
1028
|
+
[ED25519_TAG, myEd25519Hex]
|
|
1029
|
+
]
|
|
1030
|
+
});
|
|
1031
|
+
};
|
|
1032
|
+
const flushPending = () => {
|
|
1033
|
+
if (peerNostrPubkey === void 0) return;
|
|
1034
|
+
while (pending.length > 0) {
|
|
1035
|
+
const text = pending.shift();
|
|
1036
|
+
if (text !== void 0) sendEncrypted(text, peerNostrPubkey);
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
const unsubscribe = transport.subscribe({ "#t": [convTag] }, (event) => {
|
|
1040
|
+
if (closed) return;
|
|
1041
|
+
if (event.pubkey === myNostr.pubkey) return;
|
|
1042
|
+
if (event.kind === VIBEDATE_PRESENCE_KIND) {
|
|
1043
|
+
const claimedEd = tagValue(event, ED25519_TAG);
|
|
1044
|
+
if (claimedEd !== peerEd25519Hex) return;
|
|
1045
|
+
peerNostrPubkey = event.pubkey;
|
|
1046
|
+
flushPending();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (event.kind === VIBEDATE_MESSAGE_KIND) {
|
|
1050
|
+
let plaintext;
|
|
1051
|
+
try {
|
|
1052
|
+
const sender = peerNostrPubkey ?? event.pubkey;
|
|
1053
|
+
plaintext = nip04.decrypt(myNostr.sk, sender, event.content);
|
|
1054
|
+
} catch {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
peerNostrPubkey = event.pubkey;
|
|
1058
|
+
const msg = { id: event.id, text: plaintext, at: event.created_at * 1e3 };
|
|
1059
|
+
for (const cb of messageCbs) cb(msg);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
publishPresence();
|
|
1064
|
+
return {
|
|
1065
|
+
hello,
|
|
1066
|
+
send(text) {
|
|
1067
|
+
if (closed) return;
|
|
1068
|
+
if (peerNostrPubkey === void 0) {
|
|
1069
|
+
pending.push(text);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
sendEncrypted(text, peerNostrPubkey);
|
|
1073
|
+
},
|
|
1074
|
+
// ponytail: chunked media over the relay (mirror link.ts media frames as
|
|
1075
|
+
// kind-4 payloads). For v0 a relay link carries text only.
|
|
1076
|
+
async sendMedia() {
|
|
1077
|
+
return { id: "", size: 0 };
|
|
1078
|
+
},
|
|
1079
|
+
// ponytail: relay WebRTC signaling (rtc-offer/answer/ice as kind-4 payloads)
|
|
1080
|
+
// so A/V could also fall back through the relay. v0: text only.
|
|
1081
|
+
sendSignal() {
|
|
1082
|
+
},
|
|
1083
|
+
onMessage(cb) {
|
|
1084
|
+
messageCbs.add(cb);
|
|
1085
|
+
},
|
|
1086
|
+
onMedia(cb) {
|
|
1087
|
+
mediaCbs.add(cb);
|
|
1088
|
+
},
|
|
1089
|
+
onSignal(cb) {
|
|
1090
|
+
signalCbs.add(cb);
|
|
1091
|
+
},
|
|
1092
|
+
onClose(cb) {
|
|
1093
|
+
closeCbs.add(cb);
|
|
1094
|
+
},
|
|
1095
|
+
close() {
|
|
1096
|
+
if (closed) return;
|
|
1097
|
+
closed = true;
|
|
1098
|
+
try {
|
|
1099
|
+
unsubscribe();
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
transport.close();
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
async function createNostrPoolTransport(urls = DEFAULT_RELAYS) {
|
|
1110
|
+
const { SimplePool } = await nostr();
|
|
1111
|
+
const pool = new SimplePool();
|
|
1112
|
+
const relays = [...urls];
|
|
1113
|
+
return {
|
|
1114
|
+
publish(event) {
|
|
1115
|
+
try {
|
|
1116
|
+
const results = pool.publish(relays, event);
|
|
1117
|
+
Promise.allSettled(results).catch(() => {
|
|
1118
|
+
});
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
subscribe(filter, onEvent) {
|
|
1123
|
+
const closer = pool.subscribeMany(
|
|
1124
|
+
relays,
|
|
1125
|
+
filter,
|
|
1126
|
+
{ onevent: (e) => onEvent(e) }
|
|
1127
|
+
);
|
|
1128
|
+
return () => {
|
|
1129
|
+
try {
|
|
1130
|
+
closer.close();
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
},
|
|
1135
|
+
close() {
|
|
1136
|
+
try {
|
|
1137
|
+
pool.close(relays);
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
pool.destroy();
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
912
1148
|
// src/index.ts
|
|
913
1149
|
var LEAGUES = [
|
|
914
1150
|
{ name: "1M", min: 1e6, max: 4999999 },
|
|
@@ -1067,6 +1303,7 @@ export {
|
|
|
1067
1303
|
signHelloClaims,
|
|
1068
1304
|
verifyHelloClaims,
|
|
1069
1305
|
classifyHelloIdentity,
|
|
1306
|
+
loadOrCreateNostrKey,
|
|
1070
1307
|
sanitizePeerText,
|
|
1071
1308
|
TOPIC_PREFIX,
|
|
1072
1309
|
leagueTopic,
|
|
@@ -1077,6 +1314,12 @@ export {
|
|
|
1077
1314
|
recordPeer,
|
|
1078
1315
|
recordPeerMessage,
|
|
1079
1316
|
startDiscovery,
|
|
1317
|
+
DEFAULT_RELAYS,
|
|
1318
|
+
VIBEDATE_MESSAGE_KIND,
|
|
1319
|
+
VIBEDATE_PRESENCE_KIND,
|
|
1320
|
+
conversationTag,
|
|
1321
|
+
createNostrRelayLink,
|
|
1322
|
+
createNostrPoolTransport,
|
|
1080
1323
|
LEAGUES,
|
|
1081
1324
|
BELOW_LEAGUE,
|
|
1082
1325
|
league,
|
package/dist/cli.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ interface ParsedArgs {
|
|
|
17
17
|
readonly to: string | undefined;
|
|
18
18
|
/** `live --keep-alive`: stay in the swarm after stdin EOF. Default false. */
|
|
19
19
|
readonly keepAlive: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* `live --via-relay` / `discover --via-relay`: route through public Nostr relays
|
|
22
|
+
* (NIP-04 e2e) when direct hyperdht hole-punching fails. v0 explicit opt-in;
|
|
23
|
+
* auto-fallback-on-timeout is a follow-up. Default false.
|
|
24
|
+
*/
|
|
25
|
+
readonly viaRelay: boolean;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
28
|
* Parse argv (the slice AFTER the program name) into a command + options.
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMcp
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JIBC3OHV.js";
|
|
5
5
|
import {
|
|
6
6
|
CANDIDATES,
|
|
7
7
|
DEFAULT_HANDLE,
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
allLeagueNames,
|
|
13
13
|
canShareLive,
|
|
14
14
|
connectProfile,
|
|
15
|
+
createNostrPoolTransport,
|
|
16
|
+
createNostrRelayLink,
|
|
15
17
|
defaultStateDir,
|
|
16
18
|
grantLiveConsent,
|
|
17
19
|
isBlocked,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
loadBlocklist,
|
|
23
25
|
loadHandle,
|
|
24
26
|
loadOrCreateIdentity,
|
|
27
|
+
loadOrCreateNostrKey,
|
|
25
28
|
loadPeers,
|
|
26
29
|
loadProfile,
|
|
27
30
|
matches,
|
|
@@ -35,7 +38,7 @@ import {
|
|
|
35
38
|
saveHandle,
|
|
36
39
|
signHelloClaims,
|
|
37
40
|
startDiscovery
|
|
38
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-VZXPYU2C.js";
|
|
39
42
|
|
|
40
43
|
// src/cli.ts
|
|
41
44
|
import readline from "readline";
|
|
@@ -2112,7 +2115,7 @@ async function handle(req, res, opts) {
|
|
|
2112
2115
|
}
|
|
2113
2116
|
|
|
2114
2117
|
// src/cli.ts
|
|
2115
|
-
var VERSION = "0.
|
|
2118
|
+
var VERSION = "0.6.0";
|
|
2116
2119
|
function parsePort(raw) {
|
|
2117
2120
|
const n = Number(raw);
|
|
2118
2121
|
if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
|
|
@@ -2127,7 +2130,8 @@ function parseArgs(argv) {
|
|
|
2127
2130
|
any: false,
|
|
2128
2131
|
arg: void 0,
|
|
2129
2132
|
to: void 0,
|
|
2130
|
-
keepAlive: false
|
|
2133
|
+
keepAlive: false,
|
|
2134
|
+
viaRelay: false
|
|
2131
2135
|
};
|
|
2132
2136
|
for (let i = 0; i < argv.length; i++) {
|
|
2133
2137
|
const a = argv[i];
|
|
@@ -2140,7 +2144,8 @@ function parseArgs(argv) {
|
|
|
2140
2144
|
any: false,
|
|
2141
2145
|
arg: void 0,
|
|
2142
2146
|
to: void 0,
|
|
2143
|
-
keepAlive: false
|
|
2147
|
+
keepAlive: false,
|
|
2148
|
+
viaRelay: false
|
|
2144
2149
|
};
|
|
2145
2150
|
}
|
|
2146
2151
|
if (a === "--help" || a === "-h") {
|
|
@@ -2152,7 +2157,8 @@ function parseArgs(argv) {
|
|
|
2152
2157
|
any: false,
|
|
2153
2158
|
arg: void 0,
|
|
2154
2159
|
to: void 0,
|
|
2155
|
-
keepAlive: false
|
|
2160
|
+
keepAlive: false,
|
|
2161
|
+
viaRelay: false
|
|
2156
2162
|
};
|
|
2157
2163
|
}
|
|
2158
2164
|
if (a === "--live") {
|
|
@@ -2163,6 +2169,10 @@ function parseArgs(argv) {
|
|
|
2163
2169
|
out = { ...out, keepAlive: true };
|
|
2164
2170
|
continue;
|
|
2165
2171
|
}
|
|
2172
|
+
if (a === "--via-relay") {
|
|
2173
|
+
out = { ...out, viaRelay: true };
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2166
2176
|
if (a === "--any") {
|
|
2167
2177
|
out = { ...out, any: true };
|
|
2168
2178
|
continue;
|
|
@@ -2381,7 +2391,7 @@ async function cmdMatches(live) {
|
|
|
2381
2391
|
process2.stdout.write("\n");
|
|
2382
2392
|
return 0;
|
|
2383
2393
|
}
|
|
2384
|
-
async function cmdDiscover(live, any) {
|
|
2394
|
+
async function cmdDiscover(live, any, viaRelay) {
|
|
2385
2395
|
const profile = loadProfile();
|
|
2386
2396
|
if (!profile) {
|
|
2387
2397
|
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
@@ -2400,6 +2410,9 @@ async function cmdDiscover(live, any) {
|
|
|
2400
2410
|
`);
|
|
2401
2411
|
process2.stdout.write(` ${MARKS_LEGEND}
|
|
2402
2412
|
`);
|
|
2413
|
+
if (viaRelay) {
|
|
2414
|
+
process2.stdout.write(" \u2022 --via-relay: the relay fallback reaches a KNOWN peer over Nostr \u2014 use `live --via-relay --to @handle`\n");
|
|
2415
|
+
}
|
|
2403
2416
|
const { topics, acceptLeague } = discoveryScope(profile.league, any);
|
|
2404
2417
|
const session = await startDiscovery({
|
|
2405
2418
|
hello,
|
|
@@ -2489,7 +2502,100 @@ async function cmdOpen(port, any) {
|
|
|
2489
2502
|
function shouldKeepAlive(flag, stdinIsTTY) {
|
|
2490
2503
|
return flag || stdinIsTTY !== true;
|
|
2491
2504
|
}
|
|
2492
|
-
async function
|
|
2505
|
+
async function cmdLiveViaRelay(profile, to) {
|
|
2506
|
+
const target = to !== void 0 ? normalizeHandle(to) : null;
|
|
2507
|
+
if (to !== void 0 && target === null) {
|
|
2508
|
+
process2.stderr.write(`invalid target handle: ${to}
|
|
2509
|
+
`);
|
|
2510
|
+
return 1;
|
|
2511
|
+
}
|
|
2512
|
+
if (target === null) {
|
|
2513
|
+
process2.stderr.write(
|
|
2514
|
+
"relay mode (v0) targets one known peer \u2014 use: vibedating live --via-relay --to <@handle>\n(discover the peer first with `vibedating discover --live`, then reach them over the relay).\n"
|
|
2515
|
+
);
|
|
2516
|
+
return 1;
|
|
2517
|
+
}
|
|
2518
|
+
const peer = loadPeers().find((p) => sameHandle(p.handle, target) && typeof p.pubkey === "string");
|
|
2519
|
+
if (peer === void 0 || peer.pubkey === void 0) {
|
|
2520
|
+
process2.stderr.write(
|
|
2521
|
+
`${target} is not a known peer with an identity pubkey. Run \`vibedating discover --live\` first to meet them over the DHT, then retry --via-relay.
|
|
2522
|
+
`
|
|
2523
|
+
);
|
|
2524
|
+
return 1;
|
|
2525
|
+
}
|
|
2526
|
+
const identity = loadOrCreateIdentity();
|
|
2527
|
+
process2.stdout.write("\n");
|
|
2528
|
+
process2.stdout.write(` ${LIVE_NOTICE}
|
|
2529
|
+
`);
|
|
2530
|
+
process2.stdout.write(
|
|
2531
|
+
` routing e2e chat to ${sanitizePeerText(peer.handle)} through public Nostr relays (direct hole-punch fallback)
|
|
2532
|
+
`
|
|
2533
|
+
);
|
|
2534
|
+
process2.stdout.write(" \u2022 the relay stores only ciphertext \u2014 it cannot read your chat\n");
|
|
2535
|
+
const myNostr = await loadOrCreateNostrKey();
|
|
2536
|
+
const transport = await createNostrPoolTransport();
|
|
2537
|
+
const link = await createNostrRelayLink({
|
|
2538
|
+
myNostr,
|
|
2539
|
+
myEd25519Hex: identity.publicKeyHex,
|
|
2540
|
+
peerEd25519Hex: peer.pubkey,
|
|
2541
|
+
hello: peer,
|
|
2542
|
+
transport
|
|
2543
|
+
});
|
|
2544
|
+
const pairing = createPairing();
|
|
2545
|
+
pairing.onMatch((l) => {
|
|
2546
|
+
if (l === void 0) {
|
|
2547
|
+
process2.stdout.write(" \xB7 relay peer gone\n");
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
const { qual } = peerDirection(profile.league, l.hello.league);
|
|
2551
|
+
process2.stdout.write(
|
|
2552
|
+
` \xB7 relayed to ${sanitizePeerText(l.hello.handle)} (${l.hello.league}${qual} \xB7 ${l.hello.harness}) ${usageMark(l.hello)}${idMark(l.hello)}
|
|
2553
|
+
`
|
|
2554
|
+
);
|
|
2555
|
+
l.onMessage((m) => {
|
|
2556
|
+
process2.stdout.write(` <${sanitizePeerText(l.hello.handle)}> ${sanitizePeerText(m.text)}
|
|
2557
|
+
`);
|
|
2558
|
+
});
|
|
2559
|
+
});
|
|
2560
|
+
pairing.add(link);
|
|
2561
|
+
process2.stdout.write(" type to chat \xB7 /quit\n");
|
|
2562
|
+
process2.stdout.write(" (Ctrl+C to stop)\n\n");
|
|
2563
|
+
const rl = readline.createInterface({ input: process2.stdin, terminal: false });
|
|
2564
|
+
const stop = () => {
|
|
2565
|
+
rl.close();
|
|
2566
|
+
};
|
|
2567
|
+
process2.once("SIGINT", stop);
|
|
2568
|
+
process2.once("SIGTERM", stop);
|
|
2569
|
+
let quitRequested = false;
|
|
2570
|
+
for await (const line of rl) {
|
|
2571
|
+
const text = line.trim();
|
|
2572
|
+
if (text === "/quit") {
|
|
2573
|
+
quitRequested = true;
|
|
2574
|
+
break;
|
|
2575
|
+
}
|
|
2576
|
+
if (text === "/next") {
|
|
2577
|
+
process2.stdout.write(" \xB7 relay mode targets one peer \u2014 /next re-announces presence\n");
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
if (text === "") continue;
|
|
2581
|
+
const cur2 = pairing.current();
|
|
2582
|
+
if (cur2 !== void 0) {
|
|
2583
|
+
cur2.send(text);
|
|
2584
|
+
} else {
|
|
2585
|
+
process2.stdout.write(" \xB7 no relay peer yet \u2014 waiting for the presence exchange\u2026\n");
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
process2.removeListener("SIGINT", stop);
|
|
2589
|
+
process2.removeListener("SIGTERM", stop);
|
|
2590
|
+
void quitRequested;
|
|
2591
|
+
const cur = pairing.current();
|
|
2592
|
+
if (cur !== void 0) cur.close();
|
|
2593
|
+
process2.stdout.write("\n closing relay link\u2026\n");
|
|
2594
|
+
link.close();
|
|
2595
|
+
process2.stdout.write("\n");
|
|
2596
|
+
return 0;
|
|
2597
|
+
}
|
|
2598
|
+
async function cmdLive(dating, any, to, keepAlive, viaRelay) {
|
|
2493
2599
|
const profile = loadProfile();
|
|
2494
2600
|
if (!profile) {
|
|
2495
2601
|
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
@@ -2506,6 +2612,7 @@ async function cmdLive(dating, any, to, keepAlive) {
|
|
|
2506
2612
|
process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
|
|
2507
2613
|
return 1;
|
|
2508
2614
|
}
|
|
2615
|
+
if (viaRelay) return cmdLiveViaRelay(profile, to);
|
|
2509
2616
|
const hello = buildHello(profile);
|
|
2510
2617
|
process2.stdout.write("\n");
|
|
2511
2618
|
process2.stdout.write(` ${LIVE_NOTICE}
|
|
@@ -2846,10 +2953,11 @@ Usage:
|
|
|
2846
2953
|
vibedating connect Read your usage, compute + print your league
|
|
2847
2954
|
vibedating matches [--live] List candidates in your league (live peers if any)
|
|
2848
2955
|
vibedating discover [--live] [--any] Find live peers over the DHT (your league + adjacent; --any = everyone)
|
|
2849
|
-
vibedating live [--dating] [--any] [--to @handle] [--keep-alive] Live chat (your league
|
|
2956
|
+
vibedating live [--dating] [--any] [--to @handle] [--keep-alive] [--via-relay] Live chat (your league
|
|
2850
2957
|
+ adjacent; --any = everyone; /next or --dating pick;
|
|
2851
2958
|
--to targets one peer; --keep-alive survives stdin EOF \u2014
|
|
2852
|
-
automatic when stdin is not a TTY
|
|
2959
|
+
automatic when stdin is not a TTY; --via-relay routes e2e chat
|
|
2960
|
+
through public Nostr relays when direct hole-punching fails)
|
|
2853
2961
|
vibedating find <@handle> [--any] Search the DHT for one specific handle (\u2605 highlights a match)
|
|
2854
2962
|
vibedating handle [@name] Print or set your handle (persisted; a leading '@' is optional)
|
|
2855
2963
|
vibedating block <@handle> Block a handle \u2014 their hello is dropped (never recorded/paired)
|
|
@@ -2912,11 +3020,11 @@ async function main(argv) {
|
|
|
2912
3020
|
case "matches":
|
|
2913
3021
|
return cmdMatches(parsed.live);
|
|
2914
3022
|
case "discover":
|
|
2915
|
-
return cmdDiscover(parsed.live, parsed.any);
|
|
3023
|
+
return cmdDiscover(parsed.live, parsed.any, parsed.viaRelay);
|
|
2916
3024
|
case "open":
|
|
2917
3025
|
return cmdOpen(parsed.port, parsed.any);
|
|
2918
3026
|
case "live":
|
|
2919
|
-
return cmdLive(parsed.dating, parsed.any, parsed.to, parsed.keepAlive);
|
|
3027
|
+
return cmdLive(parsed.dating, parsed.any, parsed.to, parsed.keepAlive, parsed.viaRelay);
|
|
2920
3028
|
case "find":
|
|
2921
3029
|
return cmdFind(parsed.arg, parsed.any);
|
|
2922
3030
|
case "mcp":
|
package/dist/index.d.ts
CHANGED
|
@@ -238,6 +238,119 @@ interface DiscoverySession {
|
|
|
238
238
|
*/
|
|
239
239
|
declare function startDiscovery(opts: DiscoveryOptions): Promise<DiscoverySession>;
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* A Nostr event, reduced to the fields this module touches. Structurally
|
|
243
|
+
* compatible with `nostr-tools`'s `Event`, so a finalized/verified event can be
|
|
244
|
+
* published and a relay-delivered event can be received without conversion.
|
|
245
|
+
*/
|
|
246
|
+
interface RelayEvent {
|
|
247
|
+
readonly kind: number;
|
|
248
|
+
/** Sender's secp256k1 (Nostr) pubkey, hex — public; the relay sees it. */
|
|
249
|
+
readonly pubkey: string;
|
|
250
|
+
/** Ciphertext for kind-4 DMs; a presence marker for the presence kind. */
|
|
251
|
+
readonly content: string;
|
|
252
|
+
readonly tags: readonly (readonly string[])[];
|
|
253
|
+
readonly created_at: number;
|
|
254
|
+
readonly id: string;
|
|
255
|
+
readonly sig: string;
|
|
256
|
+
}
|
|
257
|
+
/** A subscription filter: kinds + a `#t` conversation-tag match. */
|
|
258
|
+
interface RelayFilter {
|
|
259
|
+
readonly kinds?: readonly number[];
|
|
260
|
+
readonly '#t'?: readonly string[];
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The injection point between the relay link and the wire. The production
|
|
264
|
+
* implementation is {@link createNostrPoolTransport} (real wss:// relays via
|
|
265
|
+
* `nostr-tools` `SimplePool`); tests pass an in-memory fan-out. A real relay
|
|
266
|
+
* STORES events and replays matching stored events to new subscribers (that
|
|
267
|
+
* store-and-replay semantics is what makes the presence handshake work across
|
|
268
|
+
* arrival order) — a faithful mock must do the same.
|
|
269
|
+
*/
|
|
270
|
+
interface RelayTransport {
|
|
271
|
+
/** Publish a signed event to the relay(s). Fire-and-forget / best-effort. */
|
|
272
|
+
publish(event: RelayEvent): void;
|
|
273
|
+
/**
|
|
274
|
+
* Subscribe to events matching `filter`. Returns an unsubscribe. A faithful
|
|
275
|
+
* relay replays matching STORED events to a brand-new subscriber before any
|
|
276
|
+
* future ones, so the presence handshake is order-independent.
|
|
277
|
+
*/
|
|
278
|
+
subscribe(filter: RelayFilter, onEvent: (event: RelayEvent) => void): () => void;
|
|
279
|
+
/** Close all relay connections. Idempotent. */
|
|
280
|
+
close(): void;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Default public Nostr relays (the public commons — we host none of them).
|
|
284
|
+
* Overridable via {@link createNostrPoolTransport}. These are well-known,
|
|
285
|
+
* generally-open relays; any reachable public relay works.
|
|
286
|
+
*/
|
|
287
|
+
declare const DEFAULT_RELAYS: readonly string[];
|
|
288
|
+
/** Kind for encrypted DM payloads — the standard NIP-04 sealed-direct-message kind. */
|
|
289
|
+
declare const VIBEDATE_MESSAGE_KIND = 4;
|
|
290
|
+
/**
|
|
291
|
+
* Kind for the presence/rendezvous ping (parameterized-replaceable range). Its
|
|
292
|
+
* content is a constant marker that reveals nothing about the conversation; its
|
|
293
|
+
* `event.pubkey` is the sender's Nostr key (public by definition), and an
|
|
294
|
+
* `ed25519` tag binds it to the sender's identity so the receiver only accepts
|
|
295
|
+
* presence from the peer it expects.
|
|
296
|
+
*/
|
|
297
|
+
declare const VIBEDATE_PRESENCE_KIND = 30078;
|
|
298
|
+
/**
|
|
299
|
+
* Derive the deterministic conversation tag two peers rendezvous on, from BOTH
|
|
300
|
+
* ed25519 identity pubkeys (sorted, hashed). Pure: both sides compute it
|
|
301
|
+
* byte-identically from data each already holds (their own identity + the
|
|
302
|
+
* peer's advertised pubkey), so NO extra exchange is needed to agree on the
|
|
303
|
+
* rendezvous point. The hash hides the underlying identities from anyone
|
|
304
|
+
* scanning the relay by tag.
|
|
305
|
+
*/
|
|
306
|
+
declare function conversationTag(myEd25519Hex: string, peerEd25519Hex: string): string;
|
|
307
|
+
/**
|
|
308
|
+
* A relay-fallback link with the SAME shape as {@link PeerLink}, so callers can
|
|
309
|
+
* drop it in wherever a direct P2P link is used (pairing, the `live` chat loop).
|
|
310
|
+
*
|
|
311
|
+
* Implemented in v0: `send`/`onMessage`/`onClose`/`close` + `.hello` (text chat
|
|
312
|
+
* over NIP-04). `sendMedia`/`sendSignal`/`onMedia`/`onSignal` are accepted as
|
|
313
|
+
* no-ops so the link is type-interchangeable with a direct PeerLink — chunked
|
|
314
|
+
* media + WebRTC signaling over the relay are `// ponytail:` follow-ups (a relay
|
|
315
|
+
* link is a fallback for peers who can't connect at all, where plain text chat
|
|
316
|
+
* is the priority).
|
|
317
|
+
*/
|
|
318
|
+
type NostrRelayLink = PeerLink;
|
|
319
|
+
/** Options for {@link createNostrRelayLink}. */
|
|
320
|
+
interface CreateNostrRelayLinkOptions {
|
|
321
|
+
/** This peer's Nostr secp256k1 keypair (see {@link loadOrCreateNostrKey}). */
|
|
322
|
+
readonly myNostr: {
|
|
323
|
+
readonly sk: Uint8Array;
|
|
324
|
+
readonly pubkey: string;
|
|
325
|
+
};
|
|
326
|
+
/** This peer's ed25519 identity pubkey (hex) — half of the convTag input. */
|
|
327
|
+
readonly myEd25519Hex: string;
|
|
328
|
+
/** The REMOTE peer's ed25519 identity pubkey (hex) — the other convTag half. */
|
|
329
|
+
readonly peerEd25519Hex: string;
|
|
330
|
+
/** The remote peer's identity, surfaced as the link's `.hello`. */
|
|
331
|
+
readonly hello: PeerHello;
|
|
332
|
+
/** The injected relay transport (real pool OR an in-memory mock). */
|
|
333
|
+
readonly transport: RelayTransport;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Build a {@link NostrRelayLink} over the injected {@link RelayTransport}. The
|
|
337
|
+
* link immediately subscribes to the conversation tag and publishes its presence
|
|
338
|
+
* so the peer can learn this side's Nostr pubkey (the bootstrap for NIP-04).
|
|
339
|
+
* Resolves once subscribed + presence published. Async only because
|
|
340
|
+
* `nostr-tools` (NIP-04 + event signing) is lazy-imported on first use.
|
|
341
|
+
*/
|
|
342
|
+
declare function createNostrRelayLink(opts: CreateNostrRelayLinkOptions): Promise<NostrRelayLink>;
|
|
343
|
+
/**
|
|
344
|
+
* Build a real {@link RelayTransport} over public Nostr relays via
|
|
345
|
+
* `nostr-tools`'s `SimplePool`. `nostr-tools` (and its WebSocket stack) is
|
|
346
|
+
* imported lazily here. Async because constructing the pool may need the
|
|
347
|
+
* lazy module load. The returned transport:
|
|
348
|
+
* - `publish` is fire-and-forget (publishes to all relays, awaits none);
|
|
349
|
+
* - `subscribe` mirrors `SimplePool.subscribeMany` and returns an unsub;
|
|
350
|
+
* - `close` tears the pool down.
|
|
351
|
+
*/
|
|
352
|
+
declare function createNostrPoolTransport(urls?: readonly string[]): Promise<RelayTransport>;
|
|
353
|
+
|
|
241
354
|
/**
|
|
242
355
|
* Persistent ed25519 identity — binds a handle to a keypair so a peer cannot
|
|
243
356
|
* impersonate it.
|
|
@@ -307,6 +420,28 @@ type IdentityVerdict = 'verified' | 'legacy' | 'drop';
|
|
|
307
420
|
* caller (discovery) turns 'drop' into "never recorded, never paired".
|
|
308
421
|
*/
|
|
309
422
|
declare function classifyHelloIdentity(hello: HelloClaims & Partial<IdentityProof>): IdentityVerdict;
|
|
423
|
+
/**
|
|
424
|
+
* A separate secp256k1 keypair for the Nostr relay fallback (see relay.ts).
|
|
425
|
+
* Kept DISTINCT from the ed25519 {@link Identity} on purpose: NIP-04 encryption
|
|
426
|
+
* requires a secp256k1 key, and reusing or deriving the ed25519 key for that
|
|
427
|
+
* would cross two unrelated cryptographic curves. The private key is persisted
|
|
428
|
+
* at `<dir>/nostr.json` (mode 0600) and never leaves the file; only the
|
|
429
|
+
* secp256k1 public key (hex) is ever published on a relay.
|
|
430
|
+
*/
|
|
431
|
+
interface NostrKey {
|
|
432
|
+
/** 32-byte secp256k1 secret key — the NIP-04 decrypt/encrypt key. Never sent. */
|
|
433
|
+
readonly sk: Uint8Array;
|
|
434
|
+
/** secp256k1 public key, hex (64 chars) — safe to publish; the relay sees it. */
|
|
435
|
+
readonly pubkey: string;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Load the persistent secp256k1 Nostr key, generating + storing it (mode 0600)
|
|
439
|
+
* on first use. A missing or corrupt file is (re)generated — never throws on
|
|
440
|
+
* disk content. Idempotent across runs: same file → same key. `nostr-tools` is
|
|
441
|
+
* imported LAZILY (only when a key must be generated) so non-relay commands
|
|
442
|
+
* never pay for the secp256k1 stack.
|
|
443
|
+
*/
|
|
444
|
+
declare function loadOrCreateNostrKey(dir?: string): Promise<NostrKey>;
|
|
310
445
|
|
|
311
446
|
/**
|
|
312
447
|
* A usage league (volume bucket). `max` is inclusive; the top tier is open-ended
|
|
@@ -423,4 +558,4 @@ declare const CANDIDATES: readonly Candidate[];
|
|
|
423
558
|
*/
|
|
424
559
|
declare function matches(myLeague: string, candidates?: readonly Candidate[]): Candidate[];
|
|
425
560
|
|
|
426
|
-
export { BELOW_LEAGUE, CANDIDATES, type Candidate, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, type HelloClaims, type Identity, type IdentityProof, type IdentityVerdict, LEAGUES, LIVE_NOTICE, type League, type LocalUsageSnapshot, type PeerHello, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, allLeagueNames, canonicalHelloClaims, classifyHelloIdentity, league, leagueIndex, leagueTopic, leaguesWithin, loadOrCreateIdentity, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, recordPeerMessage, serializeHandshake, signHelloClaims, startDiscovery, verifyHelloClaims };
|
|
561
|
+
export { BELOW_LEAGUE, CANDIDATES, type Candidate, type CreateNostrRelayLinkOptions, DEFAULT_RELAYS, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, type HelloClaims, type Identity, type IdentityProof, type IdentityVerdict, LEAGUES, LIVE_NOTICE, type League, type LocalUsageSnapshot, type NostrKey, type NostrRelayLink, type PeerHello, type RelayEvent, type RelayFilter, type RelayTransport, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, VIBEDATE_MESSAGE_KIND, VIBEDATE_PRESENCE_KIND, allLeagueNames, canonicalHelloClaims, classifyHelloIdentity, conversationTag, createNostrPoolTransport, createNostrRelayLink, league, leagueIndex, leagueTopic, leaguesWithin, loadOrCreateIdentity, loadOrCreateNostrKey, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, recordPeerMessage, serializeHandshake, signHelloClaims, startDiscovery, verifyHelloClaims };
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BELOW_LEAGUE,
|
|
3
3
|
CANDIDATES,
|
|
4
|
+
DEFAULT_RELAYS,
|
|
4
5
|
DEMO_TOTAL_TOKENS,
|
|
5
6
|
LEAGUES,
|
|
6
7
|
LIVE_NOTICE,
|
|
7
8
|
TOKENS_ENV,
|
|
8
9
|
TOPIC_PREFIX,
|
|
10
|
+
VIBEDATE_MESSAGE_KIND,
|
|
11
|
+
VIBEDATE_PRESENCE_KIND,
|
|
9
12
|
allLeagueNames,
|
|
10
13
|
canonicalHelloClaims,
|
|
11
14
|
classifyHelloIdentity,
|
|
15
|
+
conversationTag,
|
|
12
16
|
createConsentLedger,
|
|
17
|
+
createNostrPoolTransport,
|
|
18
|
+
createNostrRelayLink,
|
|
13
19
|
league,
|
|
14
20
|
leagueIndex,
|
|
15
21
|
leagueTopic,
|
|
16
22
|
leaguesWithin,
|
|
17
23
|
loadOrCreateIdentity,
|
|
24
|
+
loadOrCreateNostrKey,
|
|
18
25
|
loadPeers,
|
|
19
26
|
matches,
|
|
20
27
|
parseHandshake,
|
|
@@ -26,24 +33,31 @@ import {
|
|
|
26
33
|
signHelloClaims,
|
|
27
34
|
startDiscovery,
|
|
28
35
|
verifyHelloClaims
|
|
29
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-VZXPYU2C.js";
|
|
30
37
|
export {
|
|
31
38
|
BELOW_LEAGUE,
|
|
32
39
|
CANDIDATES,
|
|
40
|
+
DEFAULT_RELAYS,
|
|
33
41
|
DEMO_TOTAL_TOKENS,
|
|
34
42
|
LEAGUES,
|
|
35
43
|
LIVE_NOTICE,
|
|
36
44
|
TOKENS_ENV,
|
|
37
45
|
TOPIC_PREFIX,
|
|
46
|
+
VIBEDATE_MESSAGE_KIND,
|
|
47
|
+
VIBEDATE_PRESENCE_KIND,
|
|
38
48
|
allLeagueNames,
|
|
39
49
|
canonicalHelloClaims,
|
|
40
50
|
classifyHelloIdentity,
|
|
51
|
+
conversationTag,
|
|
41
52
|
createConsentLedger,
|
|
53
|
+
createNostrPoolTransport,
|
|
54
|
+
createNostrRelayLink,
|
|
42
55
|
league,
|
|
43
56
|
leagueIndex,
|
|
44
57
|
leagueTopic,
|
|
45
58
|
leaguesWithin,
|
|
46
59
|
loadOrCreateIdentity,
|
|
60
|
+
loadOrCreateNostrKey,
|
|
47
61
|
loadPeers,
|
|
48
62
|
matches,
|
|
49
63
|
parseHandshake,
|
package/dist/mcp.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibedate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
32
|
"@pooriaarab/vibe-core": "^0.3.0",
|
|
33
|
-
"hyperswarm": "^4.17.0"
|
|
33
|
+
"hyperswarm": "^4.17.0",
|
|
34
|
+
"nostr-tools": "^2.24.1"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@types/node": "^20.11.0",
|