vibedate 0.5.0 → 0.7.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-KKWP4DLY.js → chunk-HDHJLZZG.js} +343 -0
- package/dist/{chunk-I5U3O4RH.js → chunk-ZD6JBWEW.js} +1 -1
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +484 -34
- package/dist/index.d.ts +217 -1
- package/dist/index.js +21 -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,301 @@ 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
|
+
|
|
1148
|
+
// src/room.ts
|
|
1149
|
+
import { createHash as createHash3 } from "crypto";
|
|
1150
|
+
var ROOM_TOPIC_PREFIX = "vibedate-room:";
|
|
1151
|
+
function roomTopic(name) {
|
|
1152
|
+
return createHash3("sha256").update(`${ROOM_TOPIC_PREFIX}${name}`, "utf8").digest();
|
|
1153
|
+
}
|
|
1154
|
+
async function startRoom(opts) {
|
|
1155
|
+
const topic = roomTopic(opts.room);
|
|
1156
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1157
|
+
const memberHellos = /* @__PURE__ */ new Map();
|
|
1158
|
+
const messageCbs = /* @__PURE__ */ new Set();
|
|
1159
|
+
const rosterCbs = /* @__PURE__ */ new Set();
|
|
1160
|
+
const signalCbs = /* @__PURE__ */ new Set();
|
|
1161
|
+
const fireRoster = () => {
|
|
1162
|
+
const snapshot = [...memberHellos.values()];
|
|
1163
|
+
for (const cb of rosterCbs) cb(snapshot);
|
|
1164
|
+
};
|
|
1165
|
+
let discovery;
|
|
1166
|
+
const ready = startDiscovery({
|
|
1167
|
+
hello: opts.hello,
|
|
1168
|
+
// ONE topic — the room's own. Rooms are intentionally cross-league, so
|
|
1169
|
+
// every member of the room is accepted regardless of advertised league.
|
|
1170
|
+
topics: [topic],
|
|
1171
|
+
acceptLeague: () => true,
|
|
1172
|
+
...opts.isBlocked === void 0 ? {} : { isBlocked: opts.isBlocked },
|
|
1173
|
+
...opts.bootstrap === void 0 ? {} : { bootstrap: opts.bootstrap },
|
|
1174
|
+
...opts.stateDir === void 0 ? {} : { stateDir: opts.stateDir },
|
|
1175
|
+
...opts.notify === void 0 ? {} : { notify: opts.notify },
|
|
1176
|
+
onLink: (link) => {
|
|
1177
|
+
const handle = link.hello.handle;
|
|
1178
|
+
entries.set(handle, { hello: link.hello, link });
|
|
1179
|
+
memberHellos.set(handle, link.hello);
|
|
1180
|
+
fireRoster();
|
|
1181
|
+
link.onMessage((m) => {
|
|
1182
|
+
for (const cb of messageCbs) cb({ from: handle, ...m });
|
|
1183
|
+
});
|
|
1184
|
+
link.onSignal((frame) => {
|
|
1185
|
+
for (const cb of signalCbs) cb(handle, frame);
|
|
1186
|
+
});
|
|
1187
|
+
link.onClose(() => {
|
|
1188
|
+
const cur = entries.get(handle);
|
|
1189
|
+
if (cur && cur.link === link) {
|
|
1190
|
+
entries.delete(handle);
|
|
1191
|
+
memberHellos.delete(handle);
|
|
1192
|
+
fireRoster();
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}).then((s) => {
|
|
1197
|
+
discovery = s;
|
|
1198
|
+
return s.ready;
|
|
1199
|
+
});
|
|
1200
|
+
const session = {
|
|
1201
|
+
room: opts.room,
|
|
1202
|
+
topic,
|
|
1203
|
+
hello: opts.hello,
|
|
1204
|
+
members: memberHellos,
|
|
1205
|
+
ready,
|
|
1206
|
+
broadcast(text) {
|
|
1207
|
+
const reached = [];
|
|
1208
|
+
for (const [handle, entry] of entries) {
|
|
1209
|
+
entry.link.send(text);
|
|
1210
|
+
reached.push(handle);
|
|
1211
|
+
}
|
|
1212
|
+
return reached;
|
|
1213
|
+
},
|
|
1214
|
+
onMessage(cb) {
|
|
1215
|
+
messageCbs.add(cb);
|
|
1216
|
+
},
|
|
1217
|
+
onRoster(cb) {
|
|
1218
|
+
rosterCbs.add(cb);
|
|
1219
|
+
},
|
|
1220
|
+
onSignal(cb) {
|
|
1221
|
+
signalCbs.add(cb);
|
|
1222
|
+
},
|
|
1223
|
+
sendSignal(handle, frame) {
|
|
1224
|
+
entries.get(handle)?.link.sendSignal(frame);
|
|
1225
|
+
},
|
|
1226
|
+
linkFor(handle) {
|
|
1227
|
+
return entries.get(handle)?.link;
|
|
1228
|
+
},
|
|
1229
|
+
async close() {
|
|
1230
|
+
await ready.catch(() => void 0);
|
|
1231
|
+
for (const entry of entries.values()) {
|
|
1232
|
+
try {
|
|
1233
|
+
entry.link.close();
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
entries.clear();
|
|
1238
|
+
memberHellos.clear();
|
|
1239
|
+
if (discovery !== void 0) await discovery.close();
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
return session;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
912
1245
|
// src/index.ts
|
|
913
1246
|
var LEAGUES = [
|
|
914
1247
|
{ name: "1M", min: 1e6, max: 4999999 },
|
|
@@ -1067,6 +1400,7 @@ export {
|
|
|
1067
1400
|
signHelloClaims,
|
|
1068
1401
|
verifyHelloClaims,
|
|
1069
1402
|
classifyHelloIdentity,
|
|
1403
|
+
loadOrCreateNostrKey,
|
|
1070
1404
|
sanitizePeerText,
|
|
1071
1405
|
TOPIC_PREFIX,
|
|
1072
1406
|
leagueTopic,
|
|
@@ -1077,6 +1411,15 @@ export {
|
|
|
1077
1411
|
recordPeer,
|
|
1078
1412
|
recordPeerMessage,
|
|
1079
1413
|
startDiscovery,
|
|
1414
|
+
DEFAULT_RELAYS,
|
|
1415
|
+
VIBEDATE_MESSAGE_KIND,
|
|
1416
|
+
VIBEDATE_PRESENCE_KIND,
|
|
1417
|
+
conversationTag,
|
|
1418
|
+
createNostrRelayLink,
|
|
1419
|
+
createNostrPoolTransport,
|
|
1420
|
+
ROOM_TOPIC_PREFIX,
|
|
1421
|
+
roomTopic,
|
|
1422
|
+
startRoom,
|
|
1080
1423
|
LEAGUES,
|
|
1081
1424
|
BELOW_LEAGUE,
|
|
1082
1425
|
league,
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Recognized top-level commands, plus the synthetic help/version. */
|
|
3
|
-
type Command = 'connect' | 'matches' | 'discover' | 'open' | 'live' | 'find' | 'handle' | 'block' | 'unblock' | 'blocklist' | 'daemon' | 'mcp' | 'help' | 'version' | null;
|
|
3
|
+
type Command = 'connect' | 'matches' | 'discover' | 'open' | 'live' | 'find' | 'handle' | 'block' | 'unblock' | 'blocklist' | 'daemon' | 'mcp' | 'room' | 'help' | 'version' | null;
|
|
4
4
|
interface ParsedArgs {
|
|
5
5
|
readonly command: Command;
|
|
6
6
|
/** Port for `open --port`; undefined means "let the OS pick". */
|
|
@@ -17,6 +17,14 @@ 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;
|
|
26
|
+
/** `room <name>` positional, or `--room <name>`: join/create a named room. */
|
|
27
|
+
readonly room: string | undefined;
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
30
|
* Parse argv (the slice AFTER the program name) into a command + options.
|