@decentnetwork/beagle 0.1.61 → 0.1.64
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 +510 -416
- package/dist/embedded-host.d.ts +3 -0
- package/dist/embedded-host.js +20 -2
- package/dist/server.js +108 -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
|
|
@@ -1530,6 +1603,11 @@ export function startBeagleServer(opts) {
|
|
|
1530
1603
|
ok: true,
|
|
1531
1604
|
checkedAt: Date.now(),
|
|
1532
1605
|
mode: installMode(),
|
|
1606
|
+
// WHO owns the peer version. With the daemon backend it is the
|
|
1607
|
+
// agentnet/lan process, not this app — so "update beagle" cannot
|
|
1608
|
+
// move that row, and telling someone to rebuild beagle (or pull the
|
|
1609
|
+
// peer repo) is the wrong remedy for it. The lan update is.
|
|
1610
|
+
peerOwner: (opts.lanHost?.kind ?? "embedded") === "daemon" ? "daemon" : "embedded",
|
|
1533
1611
|
updates: [
|
|
1534
1612
|
row(BEAGLE_PKG, beagleVersion(), beagleLatest),
|
|
1535
1613
|
// The peer the BACKEND is running — a daemon that has been updated
|
|
@@ -1567,6 +1645,10 @@ export function startBeagleServer(opts) {
|
|
|
1567
1645
|
sendJson(res, 400, { ok: false, error: "running from a source checkout — update with: git pull && npm run build" });
|
|
1568
1646
|
return;
|
|
1569
1647
|
}
|
|
1648
|
+
// Layout we do not recognise: try the thing that works for almost
|
|
1649
|
+
// everyone rather than refusing. If it genuinely cannot install, the
|
|
1650
|
+
// npm error says so — which is still more use than a git command
|
|
1651
|
+
// against a repo the user has no access to.
|
|
1570
1652
|
if (mode === "npx") {
|
|
1571
1653
|
// npx with @latest re-resolves on every launch: restarting IS the update.
|
|
1572
1654
|
sendJson(res, 200, { ok: true, npx: true });
|
|
@@ -1644,6 +1726,20 @@ export function startBeagleServer(opts) {
|
|
|
1644
1726
|
]);
|
|
1645
1727
|
const d = (diag.ok ? diag.data : {}) ?? {};
|
|
1646
1728
|
const identity = d.identity ?? {};
|
|
1729
|
+
// A desktop install has no locally-picked punk: its avatar is whatever
|
|
1730
|
+
// it registered on beagles.eth. Advertise THAT over Carrier, so an iOS
|
|
1731
|
+
// friend sees a picture instead of nothing. Sent only when the value
|
|
1732
|
+
// CHANGES — set-profile re-pushes the profile to every established
|
|
1733
|
+
// friend, and doing that on every poll would be a packet storm.
|
|
1734
|
+
const myPunk = ensPub.get(identity.userid ?? "")?.punkId ?? null;
|
|
1735
|
+
// Only ever PUSH a real punk, never a null. A null here means "nothing
|
|
1736
|
+
// registered on beagles.eth", which is not the same as "no avatar" —
|
|
1737
|
+
// pushing it wiped a punk set directly through /api/set-profile, on
|
|
1738
|
+
// the very next poll. Registration seeds the avatar; it does not own it.
|
|
1739
|
+
if (myPunk != null && myPunk !== lastAdvertisedPunk) {
|
|
1740
|
+
lastAdvertisedPunk = myPunk;
|
|
1741
|
+
void opts.call({ op: "set-profile", punkId: myPunk }).catch(() => undefined);
|
|
1742
|
+
}
|
|
1647
1743
|
const tun = d.tun ?? {};
|
|
1648
1744
|
const node = d.node ?? {};
|
|
1649
1745
|
const diagFriends = d.friends ?? [];
|
|
@@ -1699,7 +1795,7 @@ export function startBeagleServer(opts) {
|
|
|
1699
1795
|
// toggle rather than showing a state it cannot know.
|
|
1700
1796
|
autoAccept: typeof node.autoAccept === "boolean" ? node.autoAccept : null,
|
|
1701
1797
|
avatarUrl: ensPub.get(identity.userid ?? "")?.avatarUrl ?? null,
|
|
1702
|
-
punkId:
|
|
1798
|
+
punkId: myPunk ?? node.punkId ?? null,
|
|
1703
1799
|
// The UI compares this against its baked-in __DK_UI_VERSION and
|
|
1704
1800
|
// reloads itself when they differ — an open tab otherwise runs a
|
|
1705
1801
|
// stale bundle forever after a beagle update (multi-tab call fixes
|
|
@@ -1740,7 +1836,11 @@ export function startBeagleServer(opts) {
|
|
|
1740
1836
|
lastTs: lm?.ts ?? 0,
|
|
1741
1837
|
wire: "163",
|
|
1742
1838
|
avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
|
|
1743
|
-
|
|
1839
|
+
// What they advertised over Carrier wins over the directory: it is
|
|
1840
|
+
// what they are using on their device right now, it needs no
|
|
1841
|
+
// gateway, and it is the only avatar a friend who never registered
|
|
1842
|
+
// a beagles.eth name will ever have.
|
|
1843
|
+
punkId: f.punkId ?? ensPub.get(uid)?.punkId ?? null,
|
|
1744
1844
|
};
|
|
1745
1845
|
});
|
|
1746
1846
|
const pend = pending.ok ? (pending.data?.pending ?? []) : [];
|
|
@@ -1973,8 +2073,10 @@ export function startBeagleServer(opts) {
|
|
|
1973
2073
|
return;
|
|
1974
2074
|
}
|
|
1975
2075
|
if (req.method === "POST" && url === "/api/set-profile") {
|
|
1976
|
-
|
|
1977
|
-
|
|
2076
|
+
// punkId passes through so an avatar can be set without a beagles.eth
|
|
2077
|
+
// registration — the daemon advertises it over Carrier either way.
|
|
2078
|
+
const { name, description, punkId } = await readBody(req);
|
|
2079
|
+
const r = await opts.call({ op: "set-profile", name, description, punkId });
|
|
1978
2080
|
sendJson(res, r.ok ? 200 : 400, r);
|
|
1979
2081
|
return;
|
|
1980
2082
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.64",
|
|
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",
|