@decentnetwork/beagle 0.1.61 → 0.1.63
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/carrier-node.d.ts +5 -0
- package/dist/carrier-node.js +2 -0
- package/dist/desktop/app.js +505 -415
- package/dist/embedded-host.d.ts +3 -0
- package/dist/embedded-host.js +20 -2
- package/dist/server.js +103 -6
- package/package.json +3 -3
package/dist/embedded-host.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export interface EmbeddedHostOptions {
|
|
|
18
18
|
}[];
|
|
19
19
|
nickname?: string;
|
|
20
20
|
statusMessage?: string;
|
|
21
|
+
/** Our CryptoPunks id, advertised to friends in the userinfo `gender`
|
|
22
|
+
* field — the only avatar path that does not need a server. */
|
|
23
|
+
punkId?: number;
|
|
21
24
|
autoAcceptFriends: boolean;
|
|
22
25
|
onProfileChange?: (info: {
|
|
23
26
|
name?: string;
|
package/dist/embedded-host.js
CHANGED
|
@@ -138,6 +138,9 @@ export class EmbeddedHost {
|
|
|
138
138
|
expressNodes: this.#opts.expressNodes,
|
|
139
139
|
nickname: this.#opts.nickname,
|
|
140
140
|
statusMessage: this.#opts.statusMessage,
|
|
141
|
+
// Advertised from startup, not only after a profile edit — otherwise a
|
|
142
|
+
// restart silently stops telling friends what our avatar is.
|
|
143
|
+
punkId: this.#opts.punkId,
|
|
141
144
|
fileResumeDir: resolve(this.#opts.configDir, "file-resume"),
|
|
142
145
|
});
|
|
143
146
|
this.#wire();
|
|
@@ -402,7 +405,9 @@ export class EmbeddedHost {
|
|
|
402
405
|
const id = this.#node.identity();
|
|
403
406
|
return {
|
|
404
407
|
identity: id,
|
|
405
|
-
|
|
408
|
+
// punkId so the UI can report what is actually being advertised,
|
|
409
|
+
// which is not necessarily what beagles.eth says.
|
|
410
|
+
node: { name: this.#opts.nickname ?? "", backend: "embedded", autoAccept: this.#autoAccept, punkId: this.#opts.punkId ?? null },
|
|
406
411
|
// No TUN in this backend — say so explicitly rather than omitting
|
|
407
412
|
// the key, so the UI can distinguish "no LAN" from "unknown".
|
|
408
413
|
tun: null,
|
|
@@ -448,6 +453,10 @@ export class EmbeddedHost {
|
|
|
448
453
|
f.status === "online" ||
|
|
449
454
|
this.#messages.hasInbound(f.carrierId),
|
|
450
455
|
lastSeen: f.acceptedAt,
|
|
456
|
+
// The avatar they advertised over Carrier (userinfo `gender`).
|
|
457
|
+
// Peer-to-peer, so it works for a friend who never registered a
|
|
458
|
+
// beagles.eth name — until now that meant an identicon.
|
|
459
|
+
punkId: f.punkId ?? null,
|
|
451
460
|
pinned: meta?.pinned ?? false,
|
|
452
461
|
lastMessage: lastMsg ? { dir: lastMsg.dir, text: lastMsg.text, ts: lastMsg.ts } : undefined,
|
|
453
462
|
unread: this.#messages.unreadCount(f.carrierId, meta?.lastReadTs ?? 0),
|
|
@@ -511,12 +520,21 @@ export class EmbeddedHost {
|
|
|
511
520
|
this.#meta.setAlias(uid, req.alias);
|
|
512
521
|
return {};
|
|
513
522
|
case "set-profile": {
|
|
514
|
-
const info = {
|
|
523
|
+
const info = {
|
|
524
|
+
name: req.name,
|
|
525
|
+
description: req.description,
|
|
526
|
+
// null clears, undefined leaves alone. setUserInfo re-sends the
|
|
527
|
+
// profile to every established friend, so a new avatar lands now
|
|
528
|
+
// rather than on their next reconnect.
|
|
529
|
+
punkId: req.punkId === undefined ? undefined : (req.punkId === null ? null : Number(req.punkId)),
|
|
530
|
+
};
|
|
515
531
|
this.#node.setUserInfo(info);
|
|
516
532
|
if (info.name)
|
|
517
533
|
this.#opts.nickname = info.name;
|
|
518
534
|
if (info.description !== undefined)
|
|
519
535
|
this.#opts.statusMessage = info.description;
|
|
536
|
+
if (info.punkId !== undefined)
|
|
537
|
+
this.#opts.punkId = info.punkId ?? undefined;
|
|
520
538
|
this.#opts.onProfileChange?.(info);
|
|
521
539
|
return {};
|
|
522
540
|
}
|
package/dist/server.js
CHANGED
|
@@ -273,7 +273,14 @@ function installMode() {
|
|
|
273
273
|
return "npx";
|
|
274
274
|
if (BEAGLE_ROOT.includes(join("node_modules", "@decentnetwork", "beagle")))
|
|
275
275
|
return "global";
|
|
276
|
-
|
|
276
|
+
// "dev" has to be PROVEN, not assumed. It used to be the fallthrough for
|
|
277
|
+
// any layout this function did not recognise, so a user with a perfectly
|
|
278
|
+
// ordinary install was told to run `git pull` — against a private repo they
|
|
279
|
+
// cannot clone, with no other option offered. A dead end presented as
|
|
280
|
+
// instructions.
|
|
281
|
+
if (existsSync(join(BEAGLE_ROOT, ".git")))
|
|
282
|
+
return "dev";
|
|
283
|
+
return "unknown";
|
|
277
284
|
}
|
|
278
285
|
async function npmLatest(pkg) {
|
|
279
286
|
const ctl = new AbortController();
|
|
@@ -660,6 +667,10 @@ const fmtTime = (ts) => {
|
|
|
660
667
|
return `${d.getDate()} ${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][d.getMonth()]}`;
|
|
661
668
|
};
|
|
662
669
|
const viaFromTransport = (t) => t === "udp" || t === "both" ? "direct" : t === "tcp-relay" ? "relay" : null;
|
|
670
|
+
// What we last told the daemon to advertise. Module-scoped on purpose: the
|
|
671
|
+
// value is per-process, and re-sending it on every /api/desktop poll would
|
|
672
|
+
// re-push the profile to every friend several times a second.
|
|
673
|
+
let lastAdvertisedPunk;
|
|
663
674
|
export function startBeagleServer(opts) {
|
|
664
675
|
const host = opts.listenHost ?? "127.0.0.1";
|
|
665
676
|
const port = opts.listenPort ?? 8765;
|
|
@@ -685,6 +696,32 @@ export function startBeagleServer(opts) {
|
|
|
685
696
|
const handler = async (req, res) => {
|
|
686
697
|
try {
|
|
687
698
|
const url = (req.url || "/").split("?")[0];
|
|
699
|
+
// Let Sign-in-with-Beagle pages (meet, billing, …) detect this daemon
|
|
700
|
+
// from a public https origin. Chrome Local Network Access blocks the
|
|
701
|
+
// probe unless we answer the private-network preflight.
|
|
702
|
+
const detect = url === "/api/state" || url === "/api/ping" || url === "/connect";
|
|
703
|
+
if (detect) {
|
|
704
|
+
const origin = String(req.headers.origin || "");
|
|
705
|
+
if (origin) {
|
|
706
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
707
|
+
res.setHeader("Vary", "Origin");
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
711
|
+
}
|
|
712
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
713
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
714
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
715
|
+
if (req.method === "OPTIONS") {
|
|
716
|
+
res.writeHead(204);
|
|
717
|
+
res.end();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (req.method === "GET" && url === "/api/ping") {
|
|
722
|
+
sendJson(res, 200, { ok: true });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
688
725
|
// Desktop UI bundle (the design). Falls back to the classic page when
|
|
689
726
|
// the bundle hasn't been built (dist/ui/desktop missing).
|
|
690
727
|
const desktopIndex = join(DESKTOP_DIR, "index.html");
|
|
@@ -739,7 +776,7 @@ export function startBeagleServer(opts) {
|
|
|
739
776
|
// LOCALHOST-ONLY: the popup always runs in the local user's browser
|
|
740
777
|
// (localhost), so binding the UI to a LAN IP must never let a remote
|
|
741
778
|
// host request signatures with this identity.
|
|
742
|
-
if (url === "/connect" || url === "/api/connect-approve") {
|
|
779
|
+
if (url === "/connect" || url === "/api/connect-approve" || url === "/api/launch-token") {
|
|
743
780
|
if (!isLocalRequest(req)) {
|
|
744
781
|
res.writeHead(403, { "content-type": "text/plain" });
|
|
745
782
|
res.end("Sign in with Beagle is available only on this machine (localhost).");
|
|
@@ -804,6 +841,42 @@ export function startBeagleServer(opts) {
|
|
|
804
841
|
sendJson(res, 200, { ok: true, userid, address, sig: r.data?.sig ?? "", name, avatar, punkId });
|
|
805
842
|
return;
|
|
806
843
|
}
|
|
844
|
+
// Mint the "launch an app as me" assertion the Apps tab puts in an app's
|
|
845
|
+
// URL fragment, so someone opening an app from their own client is not
|
|
846
|
+
// asked to sign in to it. LOCALHOST-ONLY like /connect: it is a
|
|
847
|
+
// signature with this identity.
|
|
848
|
+
if (req.method === "POST" && url === "/api/launch-token") {
|
|
849
|
+
const body = await readBody(req);
|
|
850
|
+
const target = validateOrigin(body.origin);
|
|
851
|
+
if (!target) {
|
|
852
|
+
sendJson(res, 400, { ok: false, error: "invalid origin" });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const ts = Date.now();
|
|
856
|
+
// A DIFFERENT prefix from decent-auth on purpose: an assertion minted
|
|
857
|
+
// here must never be replayable as a "a site asked me to sign in"
|
|
858
|
+
// proof, nor the reverse.
|
|
859
|
+
const signed = await opts.call({ op: "sign", text: `decent-launch\n${target}\n${ts}` });
|
|
860
|
+
if (!signed.ok) {
|
|
861
|
+
sendJson(res, 502, { ok: false, error: signed.error || "sign failed" });
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const diag = await opts.call({ op: "diag" });
|
|
865
|
+
const d = (diag.ok ? diag.data : {}) ?? {};
|
|
866
|
+
const identity = d.identity ?? {};
|
|
867
|
+
const node = d.node ?? {};
|
|
868
|
+
sendJson(res, 200, {
|
|
869
|
+
ok: true,
|
|
870
|
+
v: 1,
|
|
871
|
+
userid: signed.data?.userid ?? identity.userid ?? "",
|
|
872
|
+
address: identity.address ?? "",
|
|
873
|
+
name: node.name ?? "",
|
|
874
|
+
punkId: null,
|
|
875
|
+
ts,
|
|
876
|
+
sig: signed.data?.sig ?? "",
|
|
877
|
+
});
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
807
880
|
// ── beagles.eth name registration ──
|
|
808
881
|
// Register/update this node's *.beagles.eth name and bind wallet
|
|
809
882
|
// addresses into the record. The write routes trigger identity
|
|
@@ -1567,6 +1640,10 @@ export function startBeagleServer(opts) {
|
|
|
1567
1640
|
sendJson(res, 400, { ok: false, error: "running from a source checkout — update with: git pull && npm run build" });
|
|
1568
1641
|
return;
|
|
1569
1642
|
}
|
|
1643
|
+
// Layout we do not recognise: try the thing that works for almost
|
|
1644
|
+
// everyone rather than refusing. If it genuinely cannot install, the
|
|
1645
|
+
// npm error says so — which is still more use than a git command
|
|
1646
|
+
// against a repo the user has no access to.
|
|
1570
1647
|
if (mode === "npx") {
|
|
1571
1648
|
// npx with @latest re-resolves on every launch: restarting IS the update.
|
|
1572
1649
|
sendJson(res, 200, { ok: true, npx: true });
|
|
@@ -1644,6 +1721,20 @@ export function startBeagleServer(opts) {
|
|
|
1644
1721
|
]);
|
|
1645
1722
|
const d = (diag.ok ? diag.data : {}) ?? {};
|
|
1646
1723
|
const identity = d.identity ?? {};
|
|
1724
|
+
// A desktop install has no locally-picked punk: its avatar is whatever
|
|
1725
|
+
// it registered on beagles.eth. Advertise THAT over Carrier, so an iOS
|
|
1726
|
+
// friend sees a picture instead of nothing. Sent only when the value
|
|
1727
|
+
// CHANGES — set-profile re-pushes the profile to every established
|
|
1728
|
+
// friend, and doing that on every poll would be a packet storm.
|
|
1729
|
+
const myPunk = ensPub.get(identity.userid ?? "")?.punkId ?? null;
|
|
1730
|
+
// Only ever PUSH a real punk, never a null. A null here means "nothing
|
|
1731
|
+
// registered on beagles.eth", which is not the same as "no avatar" —
|
|
1732
|
+
// pushing it wiped a punk set directly through /api/set-profile, on
|
|
1733
|
+
// the very next poll. Registration seeds the avatar; it does not own it.
|
|
1734
|
+
if (myPunk != null && myPunk !== lastAdvertisedPunk) {
|
|
1735
|
+
lastAdvertisedPunk = myPunk;
|
|
1736
|
+
void opts.call({ op: "set-profile", punkId: myPunk }).catch(() => undefined);
|
|
1737
|
+
}
|
|
1647
1738
|
const tun = d.tun ?? {};
|
|
1648
1739
|
const node = d.node ?? {};
|
|
1649
1740
|
const diagFriends = d.friends ?? [];
|
|
@@ -1699,7 +1790,7 @@ export function startBeagleServer(opts) {
|
|
|
1699
1790
|
// toggle rather than showing a state it cannot know.
|
|
1700
1791
|
autoAccept: typeof node.autoAccept === "boolean" ? node.autoAccept : null,
|
|
1701
1792
|
avatarUrl: ensPub.get(identity.userid ?? "")?.avatarUrl ?? null,
|
|
1702
|
-
punkId:
|
|
1793
|
+
punkId: myPunk ?? node.punkId ?? null,
|
|
1703
1794
|
// The UI compares this against its baked-in __DK_UI_VERSION and
|
|
1704
1795
|
// reloads itself when they differ — an open tab otherwise runs a
|
|
1705
1796
|
// stale bundle forever after a beagle update (multi-tab call fixes
|
|
@@ -1740,7 +1831,11 @@ export function startBeagleServer(opts) {
|
|
|
1740
1831
|
lastTs: lm?.ts ?? 0,
|
|
1741
1832
|
wire: "163",
|
|
1742
1833
|
avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
|
|
1743
|
-
|
|
1834
|
+
// What they advertised over Carrier wins over the directory: it is
|
|
1835
|
+
// what they are using on their device right now, it needs no
|
|
1836
|
+
// gateway, and it is the only avatar a friend who never registered
|
|
1837
|
+
// a beagles.eth name will ever have.
|
|
1838
|
+
punkId: f.punkId ?? ensPub.get(uid)?.punkId ?? null,
|
|
1744
1839
|
};
|
|
1745
1840
|
});
|
|
1746
1841
|
const pend = pending.ok ? (pending.data?.pending ?? []) : [];
|
|
@@ -1973,8 +2068,10 @@ export function startBeagleServer(opts) {
|
|
|
1973
2068
|
return;
|
|
1974
2069
|
}
|
|
1975
2070
|
if (req.method === "POST" && url === "/api/set-profile") {
|
|
1976
|
-
|
|
1977
|
-
|
|
2071
|
+
// punkId passes through so an avatar can be set without a beagles.eth
|
|
2072
|
+
// registration — the daemon advertises it over Carrier either way.
|
|
2073
|
+
const { name, description, punkId } = await readBody(req);
|
|
2074
|
+
const r = await opts.call({ op: "set-profile", name, description, punkId });
|
|
1978
2075
|
sendJson(res, r.ok ? 200 : 400, r);
|
|
1979
2076
|
return;
|
|
1980
2077
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@decentnetwork/chat-components": "^0.1.3",
|
|
29
29
|
"@decentnetwork/lan": "^0.1.283",
|
|
30
|
-
"@decentnetwork/peer": "
|
|
30
|
+
"@decentnetwork/peer": "0.1.143",
|
|
31
31
|
"@decentnetwork/peer-webrtc": "^0.2.16",
|
|
32
32
|
"js-yaml": "^4.1.0",
|
|
33
33
|
"yargs": "^17.7.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@decentnetwork/beagle-ui": "0.
|
|
36
|
+
"@decentnetwork/beagle-ui": "0.2.0",
|
|
37
37
|
"@types/js-yaml": "^4.0.9",
|
|
38
38
|
"@types/node": "^20.11.0",
|
|
39
39
|
"@types/yargs": "^17.0.32",
|