@decentnetwork/beagle 0.1.121 → 0.1.123
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 +9 -0
- package/dist/carrier-node.js +7 -0
- package/dist/chat-components.d.ts +67 -0
- package/dist/chat-components.js +208 -0
- package/dist/desktop/app.js +48 -2
- package/dist/desktop/vendor/peer-webrtc.js +82 -10
- package/dist/embedded-host.js +7 -0
- package/dist/pending-requests.d.ts +28 -0
- package/dist/pending-requests.js +69 -0
- package/dist/server.js +4 -12
- package/package.json +5 -5
package/dist/carrier-node.d.ts
CHANGED
|
@@ -97,6 +97,15 @@ export declare class CarrierNode extends EventEmitter {
|
|
|
97
97
|
force?: boolean;
|
|
98
98
|
}): Promise<void>;
|
|
99
99
|
acceptFriendRequest(pubkey: string): Promise<void>;
|
|
100
|
+
/** The SDK's pending inbound requests (empty before create or on an SDK
|
|
101
|
+
* without the call). Re-read when answering friends-pending: a late
|
|
102
|
+
* profile patches the SDK's entry without firing a new event. */
|
|
103
|
+
pendingFriendRequests(): Array<{
|
|
104
|
+
pubkey: string;
|
|
105
|
+
userid?: string;
|
|
106
|
+
punkId?: number;
|
|
107
|
+
avatarUrl?: string;
|
|
108
|
+
}>;
|
|
100
109
|
removeFriend(userid: string): boolean;
|
|
101
110
|
/** Decline an inbound request and clear what it created in the SDK. */
|
|
102
111
|
rejectFriendRequest(pubkey: string): void;
|
package/dist/carrier-node.js
CHANGED
|
@@ -186,6 +186,13 @@ export class CarrierNode extends EventEmitter {
|
|
|
186
186
|
acceptFriendRequest(pubkey) {
|
|
187
187
|
return this.#require().acceptFriendRequest(pubkey);
|
|
188
188
|
}
|
|
189
|
+
/** The SDK's pending inbound requests (empty before create or on an SDK
|
|
190
|
+
* without the call). Re-read when answering friends-pending: a late
|
|
191
|
+
* profile patches the SDK's entry without firing a new event. */
|
|
192
|
+
pendingFriendRequests() {
|
|
193
|
+
const list = this.#peer?.pendingFriendRequests?.();
|
|
194
|
+
return Array.isArray(list) ? list : [];
|
|
195
|
+
}
|
|
189
196
|
removeFriend(userid) {
|
|
190
197
|
return this.#require().removeFriend(userid);
|
|
191
198
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Custom packet id for the components channel.
|
|
2
|
+
*
|
|
3
|
+
* 160-191 is toxcore's LOSSLESS range — a form must not be silently dropped.
|
|
4
|
+
* decentlan owns 161 (session), 162 (dora), 163 (IP), 164 (rate); 165 is the
|
|
5
|
+
* first free slot. Clients that don't implement this id never see the packet,
|
|
6
|
+
* which is exactly the backward-compatibility story we want. */
|
|
7
|
+
export declare const PACKET_ID_CHAT_COMPONENTS = 165;
|
|
8
|
+
/** Payload version. Receivers ignore payloads they don't know how to read
|
|
9
|
+
* rather than best-effort parsing a future shape. */
|
|
10
|
+
export declare const COMPONENTS_WIRE_VERSION = 1;
|
|
11
|
+
export declare const MAX_COMPONENTS = 12;
|
|
12
|
+
export declare const MAX_OPTIONS = 40;
|
|
13
|
+
export declare const MAX_LABEL_LEN = 120;
|
|
14
|
+
export declare const MAX_ID_LEN = 64;
|
|
15
|
+
export declare const MAX_VALUE_LEN = 512;
|
|
16
|
+
export declare const MAX_CUSTOM_ID_LEN = 128;
|
|
17
|
+
/** Hard ceiling on a decoded payload before we even try to parse it. */
|
|
18
|
+
export declare const MAX_PAYLOAD_BYTES: number;
|
|
19
|
+
export type ComponentType = "select" | "submit" | "text" | "button" | "attachment";
|
|
20
|
+
export interface SelectOption {
|
|
21
|
+
label: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ChatComponent {
|
|
25
|
+
type: ComponentType;
|
|
26
|
+
id: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
default?: string;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
options?: SelectOption[];
|
|
32
|
+
}
|
|
33
|
+
export interface ComponentsMessage {
|
|
34
|
+
v: number;
|
|
35
|
+
type: "components";
|
|
36
|
+
/** Chosen by the agent; echoed back so it can route the reply. */
|
|
37
|
+
custom_id: string;
|
|
38
|
+
components: ChatComponent[];
|
|
39
|
+
}
|
|
40
|
+
export interface InteractionMessage {
|
|
41
|
+
v: number;
|
|
42
|
+
type: "interaction";
|
|
43
|
+
custom_id: string;
|
|
44
|
+
/** id of the submit/button that fired. */
|
|
45
|
+
component: string;
|
|
46
|
+
values: Record<string, string | {
|
|
47
|
+
fileId: string;
|
|
48
|
+
name: string;
|
|
49
|
+
size: number;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate a remote components payload. Returns null when nothing renderable
|
|
54
|
+
* survives — the caller then shows only the text message, which is the correct
|
|
55
|
+
* degraded state, never an error.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseComponentsMessage(raw: unknown): ComponentsMessage | null;
|
|
58
|
+
/** Build the interaction reply. Values are the user's own input, but they are
|
|
59
|
+
* bounded here too so a pathological local state can't emit an oversized
|
|
60
|
+
* packet at the peer. */
|
|
61
|
+
export declare function buildInteraction(customId: string, component: string, values: Record<string, string>): InteractionMessage;
|
|
62
|
+
/** Decode a received packet. Returns null for anything malformed or oversized —
|
|
63
|
+
* a peer must not be able to make us throw on the receive path. */
|
|
64
|
+
export declare function decodePacket(data: Uint8Array): ComponentsMessage | InteractionMessage | null;
|
|
65
|
+
/** Validate an inbound interaction (the agent side of the channel). */
|
|
66
|
+
export declare function parseInteractionMessage(raw: unknown): InteractionMessage | null;
|
|
67
|
+
export declare function encodePacket(msg: ComponentsMessage | InteractionMessage): Uint8Array;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Brief 30 — interactive components in chat.
|
|
2
|
+
//
|
|
3
|
+
// An agent declares a form as DATA; this client renders it with its own trusted
|
|
4
|
+
// widgets. No HTML crosses the wire, no URL is fetched, no script runs. The UI's
|
|
5
|
+
// escape-everything policy (dkHtmlEscape) is untouched — components are a
|
|
6
|
+
// separate, whitelisted channel, not an escape hatch from it.
|
|
7
|
+
//
|
|
8
|
+
// Wire shape: a sibling packet, not a field on the chat message. Answering the
|
|
9
|
+
// brief's first open question that way makes "old clients still see the
|
|
10
|
+
// message" a property of the protocol rather than a rule senders must remember:
|
|
11
|
+
// the human-readable text goes out as an ordinary chat message that every
|
|
12
|
+
// existing client already renders, and the component payload rides a custom
|
|
13
|
+
// packet id that any client not built for it never even receives.
|
|
14
|
+
//
|
|
15
|
+
// EVERY value here arrives from a remote peer. The validator is the trust
|
|
16
|
+
// boundary: it whitelists types, caps every count and length, and drops what it
|
|
17
|
+
// does not understand instead of guessing.
|
|
18
|
+
/** Custom packet id for the components channel.
|
|
19
|
+
*
|
|
20
|
+
* 160-191 is toxcore's LOSSLESS range — a form must not be silently dropped.
|
|
21
|
+
* decentlan owns 161 (session), 162 (dora), 163 (IP), 164 (rate); 165 is the
|
|
22
|
+
* first free slot. Clients that don't implement this id never see the packet,
|
|
23
|
+
* which is exactly the backward-compatibility story we want. */
|
|
24
|
+
export const PACKET_ID_CHAT_COMPONENTS = 165;
|
|
25
|
+
/** Payload version. Receivers ignore payloads they don't know how to read
|
|
26
|
+
* rather than best-effort parsing a future shape. */
|
|
27
|
+
export const COMPONENTS_WIRE_VERSION = 1;
|
|
28
|
+
// --- caps -------------------------------------------------------------------
|
|
29
|
+
// "The client caps sizes/counts itself — a peer must not be able to freeze
|
|
30
|
+
// another peer's UI." (Brief 30, Security). These are deliberately small: a
|
|
31
|
+
// legitimate form is a handful of fields, and anything larger is either a bug
|
|
32
|
+
// or an attempt to hang the renderer.
|
|
33
|
+
export const MAX_COMPONENTS = 12;
|
|
34
|
+
export const MAX_OPTIONS = 40;
|
|
35
|
+
export const MAX_LABEL_LEN = 120;
|
|
36
|
+
export const MAX_ID_LEN = 64;
|
|
37
|
+
export const MAX_VALUE_LEN = 512;
|
|
38
|
+
export const MAX_CUSTOM_ID_LEN = 128;
|
|
39
|
+
/** Hard ceiling on a decoded payload before we even try to parse it. */
|
|
40
|
+
export const MAX_PAYLOAD_BYTES = 64 * 1024;
|
|
41
|
+
/** Types this build actually renders. Anything else is dropped — forward
|
|
42
|
+
* compatibility per the brief: an older client must ignore, not break. */
|
|
43
|
+
const RENDERABLE = new Set(["select", "submit", "text"]);
|
|
44
|
+
const str = (v, max) => {
|
|
45
|
+
if (typeof v !== "string")
|
|
46
|
+
return undefined;
|
|
47
|
+
const s = v.trim();
|
|
48
|
+
if (!s || s.length > max)
|
|
49
|
+
return undefined;
|
|
50
|
+
return s;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Validate a remote components payload. Returns null when nothing renderable
|
|
54
|
+
* survives — the caller then shows only the text message, which is the correct
|
|
55
|
+
* degraded state, never an error.
|
|
56
|
+
*/
|
|
57
|
+
export function parseComponentsMessage(raw) {
|
|
58
|
+
if (!raw || typeof raw !== "object")
|
|
59
|
+
return null;
|
|
60
|
+
const o = raw;
|
|
61
|
+
if (o.type !== "components")
|
|
62
|
+
return null;
|
|
63
|
+
if (o.v !== COMPONENTS_WIRE_VERSION)
|
|
64
|
+
return null;
|
|
65
|
+
const customId = str(o.custom_id, MAX_CUSTOM_ID_LEN);
|
|
66
|
+
if (!customId)
|
|
67
|
+
return null;
|
|
68
|
+
if (!Array.isArray(o.components))
|
|
69
|
+
return null;
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const entry of o.components.slice(0, MAX_COMPONENTS)) {
|
|
73
|
+
if (!entry || typeof entry !== "object")
|
|
74
|
+
continue;
|
|
75
|
+
const c = entry;
|
|
76
|
+
const type = typeof c.type === "string" ? c.type : "";
|
|
77
|
+
if (!RENDERABLE.has(type))
|
|
78
|
+
continue; // unknown/unsupported: drop, don't guess
|
|
79
|
+
const id = str(c.id, MAX_ID_LEN);
|
|
80
|
+
if (!id || seen.has(id))
|
|
81
|
+
continue; // duplicate ids would alias each other's values
|
|
82
|
+
seen.add(id);
|
|
83
|
+
const component = { type: type, id };
|
|
84
|
+
const label = str(c.label, MAX_LABEL_LEN);
|
|
85
|
+
if (label)
|
|
86
|
+
component.label = label;
|
|
87
|
+
if (c.required === true)
|
|
88
|
+
component.required = true;
|
|
89
|
+
const placeholder = str(c.placeholder, MAX_LABEL_LEN);
|
|
90
|
+
if (placeholder)
|
|
91
|
+
component.placeholder = placeholder;
|
|
92
|
+
if (type === "select") {
|
|
93
|
+
if (!Array.isArray(c.options))
|
|
94
|
+
continue; // a select with no options is unusable
|
|
95
|
+
const options = [];
|
|
96
|
+
const seenValues = new Set();
|
|
97
|
+
for (const raw of c.options.slice(0, MAX_OPTIONS)) {
|
|
98
|
+
if (!raw || typeof raw !== "object")
|
|
99
|
+
continue;
|
|
100
|
+
const opt = raw;
|
|
101
|
+
const value = str(opt.value, MAX_VALUE_LEN);
|
|
102
|
+
if (!value || seenValues.has(value))
|
|
103
|
+
continue;
|
|
104
|
+
seenValues.add(value);
|
|
105
|
+
options.push({ label: str(opt.label, MAX_LABEL_LEN) ?? value, value });
|
|
106
|
+
}
|
|
107
|
+
if (!options.length)
|
|
108
|
+
continue;
|
|
109
|
+
component.options = options;
|
|
110
|
+
// A default that isn't one of the options would submit a value the agent
|
|
111
|
+
// never offered; ignore it rather than pass it through.
|
|
112
|
+
const def = str(c.default, MAX_VALUE_LEN);
|
|
113
|
+
if (def && options.some((o2) => o2.value === def))
|
|
114
|
+
component.default = def;
|
|
115
|
+
}
|
|
116
|
+
if (type === "text") {
|
|
117
|
+
const def = str(c.default, MAX_VALUE_LEN);
|
|
118
|
+
if (def)
|
|
119
|
+
component.default = def;
|
|
120
|
+
}
|
|
121
|
+
out.push(component);
|
|
122
|
+
}
|
|
123
|
+
// A form with no way to submit can never be completed — treat it as noise.
|
|
124
|
+
if (!out.some((c) => c.type === "submit"))
|
|
125
|
+
return null;
|
|
126
|
+
// ...and a submit button with nothing to fill in is equally pointless.
|
|
127
|
+
if (!out.some((c) => c.type === "select" || c.type === "text"))
|
|
128
|
+
return null;
|
|
129
|
+
return { v: COMPONENTS_WIRE_VERSION, type: "components", custom_id: customId, components: out };
|
|
130
|
+
}
|
|
131
|
+
/** Build the interaction reply. Values are the user's own input, but they are
|
|
132
|
+
* bounded here too so a pathological local state can't emit an oversized
|
|
133
|
+
* packet at the peer. */
|
|
134
|
+
export function buildInteraction(customId, component, values) {
|
|
135
|
+
const clean = {};
|
|
136
|
+
for (const [k, v] of Object.entries(values)) {
|
|
137
|
+
const key = str(k, MAX_ID_LEN);
|
|
138
|
+
if (!key)
|
|
139
|
+
continue;
|
|
140
|
+
clean[key] = typeof v === "string" ? v.slice(0, MAX_VALUE_LEN) : "";
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
v: COMPONENTS_WIRE_VERSION,
|
|
144
|
+
type: "interaction",
|
|
145
|
+
custom_id: customId.slice(0, MAX_CUSTOM_ID_LEN),
|
|
146
|
+
component: component.slice(0, MAX_ID_LEN),
|
|
147
|
+
values: clean,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Decode a received packet. Returns null for anything malformed or oversized —
|
|
151
|
+
* a peer must not be able to make us throw on the receive path. */
|
|
152
|
+
export function decodePacket(data) {
|
|
153
|
+
if (!data?.length || data.length > MAX_PAYLOAD_BYTES)
|
|
154
|
+
return null;
|
|
155
|
+
let parsed;
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(Buffer.from(data).toString("utf-8"));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const o = parsed;
|
|
163
|
+
if (!o || typeof o !== "object")
|
|
164
|
+
return null;
|
|
165
|
+
if (o.type === "components")
|
|
166
|
+
return parseComponentsMessage(o);
|
|
167
|
+
if (o.type === "interaction")
|
|
168
|
+
return parseInteractionMessage(o);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
/** Validate an inbound interaction (the agent side of the channel). */
|
|
172
|
+
export function parseInteractionMessage(raw) {
|
|
173
|
+
if (!raw || typeof raw !== "object")
|
|
174
|
+
return null;
|
|
175
|
+
const o = raw;
|
|
176
|
+
if (o.type !== "interaction" || o.v !== COMPONENTS_WIRE_VERSION)
|
|
177
|
+
return null;
|
|
178
|
+
const customId = str(o.custom_id, MAX_CUSTOM_ID_LEN);
|
|
179
|
+
const component = str(o.component, MAX_ID_LEN);
|
|
180
|
+
if (!customId || !component)
|
|
181
|
+
return null;
|
|
182
|
+
const values = {};
|
|
183
|
+
if (o.values && typeof o.values === "object") {
|
|
184
|
+
for (const [k, v] of Object.entries(o.values).slice(0, MAX_COMPONENTS)) {
|
|
185
|
+
const key = str(k, MAX_ID_LEN);
|
|
186
|
+
if (!key)
|
|
187
|
+
continue;
|
|
188
|
+
if (typeof v === "string") {
|
|
189
|
+
values[key] = v.slice(0, MAX_VALUE_LEN);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// File values are {fileId, name, size} — fileId is the content sha256,
|
|
193
|
+
// which is what makes the reference exact (Brief 30 §3).
|
|
194
|
+
if (v && typeof v === "object") {
|
|
195
|
+
const f = v;
|
|
196
|
+
const fileId = str(f.fileId, 128);
|
|
197
|
+
const name = str(f.name, MAX_LABEL_LEN);
|
|
198
|
+
const size = typeof f.size === "number" && Number.isFinite(f.size) ? f.size : undefined;
|
|
199
|
+
if (fileId && name && size !== undefined)
|
|
200
|
+
values[key] = { fileId, name, size };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { v: COMPONENTS_WIRE_VERSION, type: "interaction", custom_id: customId, component, values };
|
|
205
|
+
}
|
|
206
|
+
export function encodePacket(msg) {
|
|
207
|
+
return new Uint8Array(Buffer.from(JSON.stringify(msg), "utf-8"));
|
|
208
|
+
}
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.123";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -1697,6 +1697,26 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
|
1697
1697
|
}, [append, lang, onReloadFriends]);
|
|
1698
1698
|
return { thread, handle, removeFriends, adoptServicePeers };
|
|
1699
1699
|
}
|
|
1700
|
+
var MAX_URL = 2048;
|
|
1701
|
+
function requestAvatarUrl(r) {
|
|
1702
|
+
const u = r && r.avatarUrl;
|
|
1703
|
+
if (typeof u !== "string" || !u || u.length > MAX_URL)
|
|
1704
|
+
return null;
|
|
1705
|
+
return /^https:\/\/[^\s"'<>]+$/i.test(u) ? u : null;
|
|
1706
|
+
}
|
|
1707
|
+
function requestPunkId(r) {
|
|
1708
|
+
const p = r && r.punkId;
|
|
1709
|
+
return typeof p === "number" && Number.isInteger(p) && p >= 0 ? p : null;
|
|
1710
|
+
}
|
|
1711
|
+
function requestAvatarSource(r, skipUrl = false) {
|
|
1712
|
+
const url = skipUrl ? null : requestAvatarUrl(r);
|
|
1713
|
+
if (url)
|
|
1714
|
+
return { kind: "url", url };
|
|
1715
|
+
const id = requestPunkId(r);
|
|
1716
|
+
if (id != null)
|
|
1717
|
+
return { kind: "punk", id };
|
|
1718
|
+
return { kind: "ens" };
|
|
1719
|
+
}
|
|
1700
1720
|
var DK_CMD_HIST_KEY = "beagle.assistant.history";
|
|
1701
1721
|
var DK_CMD_HIST_MAX = 50;
|
|
1702
1722
|
function RequestsBlock({ T, requests, onAct }) {
|
|
@@ -1740,7 +1760,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
|
1740
1760
|
display: "flex",
|
|
1741
1761
|
flexDirection: "column",
|
|
1742
1762
|
gap: 8
|
|
1743
|
-
} }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(
|
|
1763
|
+
} }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkRequestAvatar, { r, size: 30, radius: 7 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, r.name ? /* @__PURE__ */ React.createElement("div", { style: {
|
|
1744
1764
|
fontFamily: "var(--ui)",
|
|
1745
1765
|
fontSize: 13,
|
|
1746
1766
|
fontWeight: 600,
|
|
@@ -1757,6 +1777,32 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
|
1757
1777
|
paddingLeft: 8
|
|
1758
1778
|
} }, r.descr), failed[r.id] && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--danger)" } }, failed[r.id]), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => act(r, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => act(r, "reject"), style: { flex: 1 } }, T.reject))))));
|
|
1759
1779
|
}
|
|
1780
|
+
function DkRequestAvatar({ r, size, radius }) {
|
|
1781
|
+
const [urlBroken, setUrlBroken] = React.useState(false);
|
|
1782
|
+
const url = requestAvatarUrl(r);
|
|
1783
|
+
React.useEffect(() => {
|
|
1784
|
+
setUrlBroken(false);
|
|
1785
|
+
}, [url]);
|
|
1786
|
+
const src = requestAvatarSource(r, urlBroken);
|
|
1787
|
+
const seed = r.carrier || r.userid || r.id;
|
|
1788
|
+
if (src.kind === "url") {
|
|
1789
|
+
return /* @__PURE__ */ React.createElement(
|
|
1790
|
+
"img",
|
|
1791
|
+
{
|
|
1792
|
+
src: src.url,
|
|
1793
|
+
alt: "",
|
|
1794
|
+
width: size,
|
|
1795
|
+
height: size,
|
|
1796
|
+
referrerPolicy: "no-referrer",
|
|
1797
|
+
onError: () => setUrlBroken(true),
|
|
1798
|
+
style: { width: size, height: size, borderRadius: radius, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" }
|
|
1799
|
+
}
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
if (src.kind === "punk")
|
|
1803
|
+
return /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: src.id, size, radius, fallbackSeed: seed });
|
|
1804
|
+
return /* @__PURE__ */ React.createElement(DkEnsAvatar, { userid: r.userid || r.id, name: r.name, fallbackSeed: r.carrier, size, radius });
|
|
1805
|
+
}
|
|
1760
1806
|
function PeerRow({ peer, T, active, onClick, muted, isProtocol, fix }) {
|
|
1761
1807
|
const name = peer.alias || peer.userId;
|
|
1762
1808
|
const row = rosterRow(peer, isProtocol || (() => false), fix);
|
|
@@ -57,6 +57,8 @@ var PeerWebRTC = (() => {
|
|
|
57
57
|
fitSignalForInvite: () => fitSignalForInvite,
|
|
58
58
|
looksLikeSignal: () => looksLikeSignal,
|
|
59
59
|
mediaDirection: () => mediaDirection,
|
|
60
|
+
orphanPayloadTypeLines: () => orphanPayloadTypeLines,
|
|
61
|
+
sanitizeSdp: () => sanitizeSdp,
|
|
60
62
|
shrinkSdpForInvite: () => shrinkSdpForInvite,
|
|
61
63
|
videoIsSending: () => videoIsSending
|
|
62
64
|
});
|
|
@@ -198,6 +200,29 @@ var PeerWebRTC = (() => {
|
|
|
198
200
|
}
|
|
199
201
|
return join(lines);
|
|
200
202
|
}
|
|
203
|
+
function orphanPayloadTypeLines(sdp) {
|
|
204
|
+
const out = [];
|
|
205
|
+
let pts;
|
|
206
|
+
for (const line of sdp.replace(/\r\n/g, "\n").split("\n")) {
|
|
207
|
+
if (line.startsWith("m=")) {
|
|
208
|
+
pts = new Set(line.split(" ").slice(3));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const m = pts && /^a=(?:rtpmap|rtcp-fb|fmtp):(\d+)\b/.exec(line);
|
|
212
|
+
if (m && !pts.has(m[1]))
|
|
213
|
+
out.push(line);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
function sanitizeSdp(sdp) {
|
|
218
|
+
const orphans = orphanPayloadTypeLines(sdp);
|
|
219
|
+
if (orphans.length === 0)
|
|
220
|
+
return sdp;
|
|
221
|
+
const drop = new Set(orphans);
|
|
222
|
+
const crlf = sdp.includes("\r\n");
|
|
223
|
+
const kept = sdp.replace(/\r\n/g, "\n").split("\n").filter((l) => !drop.has(l));
|
|
224
|
+
return kept.join(crlf ? "\r\n" : "\n");
|
|
225
|
+
}
|
|
201
226
|
|
|
202
227
|
// node_modules/@decentnetwork/peer-webrtc/dist/signal.js
|
|
203
228
|
var SDP_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -297,7 +322,7 @@ var PeerWebRTC = (() => {
|
|
|
297
322
|
var ICE_RESTART_MAX = 3;
|
|
298
323
|
var ICE_DISCONNECT_GRACE_MS = 4e3;
|
|
299
324
|
var ICE_FAILED_GRACE_MS = 12e4;
|
|
300
|
-
var _opts, _iceServers, _sessions, _restartOffersEnabled, _setOutgoingVideoTrack, setOutgoingVideoTrack_fn, _renegotiate, renegotiate_fn, _createOffer, createOffer_fn, _createAnswer, createAnswer_fn, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn, _restartIce, restartIce_fn, _handleSignal, handleSignal_fn, _onOffer, onOffer_fn, _answerRenegotiation, answerRenegotiation_fn, _onAnswer, onAnswer_fn, _onCandidate, onCandidate_fn, _onBye, onBye_fn, _onAuxiliary, onAuxiliary_fn, _createSession, createSession_fn, _wirePeerConnection, wirePeerConnection_fn, _ensureRemoteStream, ensureRemoteStream_fn, _attachLocalMedia, attachLocalMedia_fn, _flushLocalCandidates, flushLocalCandidates_fn, _drainRemoteCandidates, drainRemoteCandidates_fn, _send, send_fn, _sendByeSafe, sendByeSafe_fn, _cleanup, cleanup_fn, _setState, setState_fn, _normId, normId_fn, _toInfo, _newCallId, newCallId_fn, _log, log_fn;
|
|
325
|
+
var _opts, _iceServers, _sessions, _restartOffersEnabled, _setOutgoingVideoTrack, setOutgoingVideoTrack_fn, _renegotiate, renegotiate_fn, _createOffer, createOffer_fn, _createAnswer, createAnswer_fn, _cleanOutbound, cleanOutbound_fn, _cleanInbound, cleanInbound_fn, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn, _restartIce, restartIce_fn, _handleSignal, handleSignal_fn, _onOffer, onOffer_fn, _answerRenegotiation, answerRenegotiation_fn, _negotiationFailed, negotiationFailed_fn, _onAnswer, onAnswer_fn, _onCandidate, onCandidate_fn, _onBye, onBye_fn, _onAuxiliary, onAuxiliary_fn, _createSession, createSession_fn, _wirePeerConnection, wirePeerConnection_fn, _ensureRemoteStream, ensureRemoteStream_fn, _attachLocalMedia, attachLocalMedia_fn, _flushLocalCandidates, flushLocalCandidates_fn, _drainRemoteCandidates, drainRemoteCandidates_fn, _send, send_fn, _sendByeSafe, sendByeSafe_fn, _cleanup, cleanup_fn, _setState, setState_fn, _normId, normId_fn, _toInfo, _newCallId, newCallId_fn, _log, log_fn;
|
|
301
326
|
var CallEngine = class extends Emitter {
|
|
302
327
|
constructor(opts) {
|
|
303
328
|
var _a, _b;
|
|
@@ -309,10 +334,31 @@ var PeerWebRTC = (() => {
|
|
|
309
334
|
/** Create + send a fresh offer to renegotiate media (e.g. after adding a
|
|
310
335
|
* screen-share track). The peer applies it via #onOffer's renegotiation path. */
|
|
311
336
|
__privateAdd(this, _renegotiate);
|
|
312
|
-
/** createOffer with the lean codec set applied (see #preferLeanVideoCodecs)
|
|
337
|
+
/** createOffer with the lean codec set applied (see #preferLeanVideoCodecs)
|
|
338
|
+
* and the result sanitized (see #cleanOutbound). Every offer — initial,
|
|
339
|
+
* screen-share re-offer, ICE restart — goes through here. */
|
|
313
340
|
__privateAdd(this, _createOffer);
|
|
314
|
-
/** createAnswer with the lean codec set applied. */
|
|
341
|
+
/** createAnswer with the lean codec set applied and the result sanitized. */
|
|
315
342
|
__privateAdd(this, _createAnswer);
|
|
343
|
+
/**
|
|
344
|
+
* Strip orphan codec lines from a description WE produced, before it is
|
|
345
|
+
* applied locally and before it is sent.
|
|
346
|
+
*
|
|
347
|
+
* Firefox's setCodecPreferences drops VP9/AV1 from the m=video line but
|
|
348
|
+
* leaves their `a=fmtp:` / `a=rtcp-fb:` lines in place; Chrome's parser then
|
|
349
|
+
* rejects the whole re-offer ("Failed to parse codecs correctly") and the
|
|
350
|
+
* Firefox peer's screen share never shows up. Measured 2026-10-31 between
|
|
351
|
+
* the Firefox extension and a Chrome desktop, both on 0.2.16. The sanitizer
|
|
352
|
+
* is a no-op on a clean description (Chrome, Safari), so the same object
|
|
353
|
+
* comes back untouched there.
|
|
354
|
+
*/
|
|
355
|
+
__privateAdd(this, _cleanOutbound);
|
|
356
|
+
/**
|
|
357
|
+
* The same cleanup for a description the PEER sent, before setRemoteDescription:
|
|
358
|
+
* Firefox extensions already in the field (AMO 1.4.14 ships 0.2.16) still
|
|
359
|
+
* produce the orphan lines, and a receiver on this engine must accept them.
|
|
360
|
+
*/
|
|
361
|
+
__privateAdd(this, _cleanInbound);
|
|
316
362
|
/**
|
|
317
363
|
* Restrict video to the codecs the peers actually negotiate, so the SDP fits
|
|
318
364
|
* the 8192-byte Carrier invite channel. Chrome otherwise advertises VP9 and
|
|
@@ -335,6 +381,7 @@ var PeerWebRTC = (() => {
|
|
|
335
381
|
__privateAdd(this, _onOffer);
|
|
336
382
|
/** Apply a mid-call re-offer (renegotiation) and send the answer. */
|
|
337
383
|
__privateAdd(this, _answerRenegotiation);
|
|
384
|
+
__privateAdd(this, _negotiationFailed);
|
|
338
385
|
__privateAdd(this, _onAnswer);
|
|
339
386
|
__privateAdd(this, _onCandidate);
|
|
340
387
|
__privateAdd(this, _onBye);
|
|
@@ -450,7 +497,7 @@ var PeerWebRTC = (() => {
|
|
|
450
497
|
throw new Error(`accept: no pending incoming call ${callId}`);
|
|
451
498
|
}
|
|
452
499
|
await __privateMethod(this, _attachLocalMedia, attachLocalMedia_fn).call(this, session);
|
|
453
|
-
await session.pc.setRemoteDescription({ type: "offer", sdp: session.pendingOffer });
|
|
500
|
+
await session.pc.setRemoteDescription({ type: "offer", sdp: __privateMethod(this, _cleanInbound, cleanInbound_fn).call(this, session.pendingOffer, "offer") });
|
|
454
501
|
session.hasReceivedSdp = true;
|
|
455
502
|
session.pendingOffer = void 0;
|
|
456
503
|
await __privateMethod(this, _drainRemoteCandidates, drainRemoteCandidates_fn).call(this, session);
|
|
@@ -638,12 +685,30 @@ var PeerWebRTC = (() => {
|
|
|
638
685
|
_createOffer = new WeakSet();
|
|
639
686
|
createOffer_fn = async function(session, opts) {
|
|
640
687
|
__privateMethod(this, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn).call(this, session.pc);
|
|
641
|
-
return session.pc.createOffer(opts);
|
|
688
|
+
return __privateMethod(this, _cleanOutbound, cleanOutbound_fn).call(this, await session.pc.createOffer(opts));
|
|
642
689
|
};
|
|
643
690
|
_createAnswer = new WeakSet();
|
|
644
691
|
createAnswer_fn = async function(session) {
|
|
645
692
|
__privateMethod(this, _preferLeanVideoCodecs, preferLeanVideoCodecs_fn).call(this, session.pc);
|
|
646
|
-
return session.pc.createAnswer();
|
|
693
|
+
return __privateMethod(this, _cleanOutbound, cleanOutbound_fn).call(this, await session.pc.createAnswer());
|
|
694
|
+
};
|
|
695
|
+
_cleanOutbound = new WeakSet();
|
|
696
|
+
cleanOutbound_fn = function(desc) {
|
|
697
|
+
var _a, _b;
|
|
698
|
+
const raw = (_a = desc.sdp) != null ? _a : "";
|
|
699
|
+
const sdp = sanitizeSdp(raw);
|
|
700
|
+
if (sdp === raw)
|
|
701
|
+
return desc;
|
|
702
|
+
__privateMethod(this, _log, log_fn).call(this, `${(_b = desc.type) != null ? _b : "sdp"}: removed ${orphanPayloadTypeLines(raw).length} orphan codec line(s) from our own description`);
|
|
703
|
+
return { type: desc.type, sdp };
|
|
704
|
+
};
|
|
705
|
+
_cleanInbound = new WeakSet();
|
|
706
|
+
cleanInbound_fn = function(raw, what) {
|
|
707
|
+
const sdp = sanitizeSdp(raw);
|
|
708
|
+
if (sdp !== raw) {
|
|
709
|
+
__privateMethod(this, _log, log_fn).call(this, `${what}: removed ${orphanPayloadTypeLines(raw).length} orphan codec line(s) from the peer's description`);
|
|
710
|
+
}
|
|
711
|
+
return sdp;
|
|
647
712
|
};
|
|
648
713
|
_preferLeanVideoCodecs = new WeakSet();
|
|
649
714
|
preferLeanVideoCodecs_fn = function(pc) {
|
|
@@ -733,14 +798,21 @@ var PeerWebRTC = (() => {
|
|
|
733
798
|
_answerRenegotiation = new WeakSet();
|
|
734
799
|
answerRenegotiation_fn = async function(session, sdp) {
|
|
735
800
|
try {
|
|
736
|
-
await session.pc.setRemoteDescription({ type: "offer", sdp });
|
|
801
|
+
await session.pc.setRemoteDescription({ type: "offer", sdp: __privateMethod(this, _cleanInbound, cleanInbound_fn).call(this, sdp, "offer") });
|
|
737
802
|
const answer = await __privateMethod(this, _createAnswer, createAnswer_fn).call(this, session);
|
|
738
803
|
await session.pc.setLocalDescription(answer);
|
|
739
804
|
await __privateMethod(this, _send, send_fn).call(this, session.peerId, { type: "answer", sdp: answer.sdp, callId: session.callId });
|
|
740
805
|
} catch (err) {
|
|
741
|
-
__privateMethod(this,
|
|
806
|
+
__privateMethod(this, _negotiationFailed, negotiationFailed_fn).call(this, session, "re-offer", err);
|
|
742
807
|
}
|
|
743
808
|
};
|
|
809
|
+
_negotiationFailed = new WeakSet();
|
|
810
|
+
negotiationFailed_fn = function(session, what, err) {
|
|
811
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
812
|
+
__privateMethod(this, _log, log_fn).call(this, `renegotiation failed (${what}) callId=${session.callId}: ${error.message}`);
|
|
813
|
+
console.warn(`[peer-webrtc] could not apply the peer's ${what} for call ${session.callId}: ${error.message}`);
|
|
814
|
+
this.emit("negotiationFailed", session.callId, error);
|
|
815
|
+
};
|
|
744
816
|
_onAnswer = new WeakSet();
|
|
745
817
|
onAnswer_fn = function(peerId, signal) {
|
|
746
818
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _normId, normId_fn).call(this, signal.callId));
|
|
@@ -749,14 +821,14 @@ var PeerWebRTC = (() => {
|
|
|
749
821
|
session.peerId = peerId;
|
|
750
822
|
void (async () => {
|
|
751
823
|
try {
|
|
752
|
-
await session.pc.setRemoteDescription({ type: "answer", sdp: signal.sdp });
|
|
824
|
+
await session.pc.setRemoteDescription({ type: "answer", sdp: __privateMethod(this, _cleanInbound, cleanInbound_fn).call(this, signal.sdp, "answer") });
|
|
753
825
|
session.hasReceivedSdp = true;
|
|
754
826
|
if (session.state !== "connected")
|
|
755
827
|
__privateMethod(this, _setState, setState_fn).call(this, session, "connecting");
|
|
756
828
|
await __privateMethod(this, _drainRemoteCandidates, drainRemoteCandidates_fn).call(this, session);
|
|
757
829
|
await __privateMethod(this, _flushLocalCandidates, flushLocalCandidates_fn).call(this, session);
|
|
758
830
|
} catch (err) {
|
|
759
|
-
__privateMethod(this,
|
|
831
|
+
__privateMethod(this, _negotiationFailed, negotiationFailed_fn).call(this, session, "answer", err);
|
|
760
832
|
}
|
|
761
833
|
})();
|
|
762
834
|
};
|
package/dist/embedded-host.js
CHANGED
|
@@ -18,6 +18,7 @@ import { MessageStore } from "./message-store.js";
|
|
|
18
18
|
import { FriendMetaStore } from "./friend-meta.js";
|
|
19
19
|
import { Logger } from "./logger.js";
|
|
20
20
|
import { acquireIdentityLock } from "./identity-lock.js";
|
|
21
|
+
import { refreshPendingAvatars, requesterAvatar } from "./pending-requests.js";
|
|
21
22
|
/** Files bigger than this are not queued for an offline friend — the bytes sit
|
|
22
23
|
* on disk until they reconnect, and an unbounded queue is how you fill a
|
|
23
24
|
* user's disk by accident. Live transfers have no such limit. */
|
|
@@ -228,6 +229,9 @@ export class EmbeddedHost {
|
|
|
228
229
|
name: req.name,
|
|
229
230
|
descr: req.description,
|
|
230
231
|
hello: req.hello,
|
|
232
|
+
// The requester's punk / avatar URL (peer >= 0.1.170), so the card can
|
|
233
|
+
// draw it before accept. A later profile is picked up in friends-pending.
|
|
234
|
+
...requesterAvatar(req),
|
|
231
235
|
arrivedAt: Date.now(),
|
|
232
236
|
});
|
|
233
237
|
this.#events.emit("event", { type: "friend-request", userid });
|
|
@@ -493,6 +497,9 @@ export class EmbeddedHost {
|
|
|
493
497
|
return {};
|
|
494
498
|
}
|
|
495
499
|
case "friends-pending":
|
|
500
|
+
// Re-read the SDK's list: a requester's profile that arrived after its
|
|
501
|
+
// request patches only the SDK's entry, with no new event.
|
|
502
|
+
refreshPendingAvatars(this.#pending.values(), this.#node.pendingFriendRequests());
|
|
496
503
|
return { pending: [...this.#pending.values()] };
|
|
497
504
|
case "friends-accept": {
|
|
498
505
|
const entry = this.#pending.get(uid);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RequesterAvatar {
|
|
2
|
+
punkId?: number;
|
|
3
|
+
avatarUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
/** The valid avatar fields of an SDK / backend friend-request record. Keys are
|
|
6
|
+
* present only with a value; punk 0 is a real CryptoPunk. */
|
|
7
|
+
export declare function requesterAvatar(src: {
|
|
8
|
+
punkId?: unknown;
|
|
9
|
+
avatarUrl?: unknown;
|
|
10
|
+
} | undefined | null): RequesterAvatar;
|
|
11
|
+
/** Fold the SDK's current pending list into the host's own entries, matched by
|
|
12
|
+
* pubkey or userid. A value the SDK has wins (a late profile can change it); a
|
|
13
|
+
* value only the host has is kept. Mutates `entries`; returns whether anything
|
|
14
|
+
* changed. */
|
|
15
|
+
export declare function refreshPendingAvatars<E extends {
|
|
16
|
+
userid: string;
|
|
17
|
+
pubkey: string;
|
|
18
|
+
} & RequesterAvatar>(entries: Iterable<E>, live: ReadonlyArray<{
|
|
19
|
+
pubkey?: string;
|
|
20
|
+
userid?: string;
|
|
21
|
+
punkId?: unknown;
|
|
22
|
+
avatarUrl?: unknown;
|
|
23
|
+
}>): boolean;
|
|
24
|
+
/** One `/api/desktop` `requests[]` row from a friends-pending entry (daemon or
|
|
25
|
+
* embedded). `punkId` / `avatarUrl` are null when the requester sent none —
|
|
26
|
+
* the same convention as `peers[]` — and the card then falls back to the
|
|
27
|
+
* beagles.eth lookup and a generated picture. */
|
|
28
|
+
export declare function desktopRequestRow(p: Record<string, unknown>, i: number, fmtTime: (ts?: number) => string): Record<string, unknown>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The requester's avatar on a pending friend request, before anyone accepts.
|
|
2
|
+
//
|
|
3
|
+
// peer >= 0.1.170 carries the requester's punk (friendreq field 3, `gender`)
|
|
4
|
+
// and custom avatar URL (field 4) in the request itself, and fills them from a
|
|
5
|
+
// profile the requester pushed before accept. When that profile arrives after
|
|
6
|
+
// the request, the SDK patches only its own pending list — no new event — so
|
|
7
|
+
// a host that copied the event once must re-read peer.pendingFriendRequests()
|
|
8
|
+
// to learn it. lan >= 0.1.313 does that for a daemon backend; the embedded host
|
|
9
|
+
// does it here. See decent-hq docs/tasks/TASK-2026-11-01-avatar-on-friend-request.md.
|
|
10
|
+
import { punkIdOf } from "./friend-store.js";
|
|
11
|
+
/** The valid avatar fields of an SDK / backend friend-request record. Keys are
|
|
12
|
+
* present only with a value; punk 0 is a real CryptoPunk. */
|
|
13
|
+
export function requesterAvatar(src) {
|
|
14
|
+
const out = {};
|
|
15
|
+
if (!src)
|
|
16
|
+
return out;
|
|
17
|
+
const punk = punkIdOf(src.punkId);
|
|
18
|
+
if (punk !== undefined && Number.isInteger(punk))
|
|
19
|
+
out.punkId = punk;
|
|
20
|
+
if (typeof src.avatarUrl === "string" && src.avatarUrl.length > 0)
|
|
21
|
+
out.avatarUrl = src.avatarUrl;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** Fold the SDK's current pending list into the host's own entries, matched by
|
|
25
|
+
* pubkey or userid. A value the SDK has wins (a late profile can change it); a
|
|
26
|
+
* value only the host has is kept. Mutates `entries`; returns whether anything
|
|
27
|
+
* changed. */
|
|
28
|
+
export function refreshPendingAvatars(entries, live) {
|
|
29
|
+
if (!live.length)
|
|
30
|
+
return false;
|
|
31
|
+
let changed = false;
|
|
32
|
+
for (const e of entries) {
|
|
33
|
+
const l = live.find((x) => (!!x.pubkey && x.pubkey === e.pubkey) || (!!x.userid && x.userid === e.userid));
|
|
34
|
+
if (!l)
|
|
35
|
+
continue;
|
|
36
|
+
const a = requesterAvatar(l);
|
|
37
|
+
if (a.punkId !== undefined && a.punkId !== e.punkId) {
|
|
38
|
+
e.punkId = a.punkId;
|
|
39
|
+
changed = true;
|
|
40
|
+
}
|
|
41
|
+
if (a.avatarUrl !== undefined && a.avatarUrl !== e.avatarUrl) {
|
|
42
|
+
e.avatarUrl = a.avatarUrl;
|
|
43
|
+
changed = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return changed;
|
|
47
|
+
}
|
|
48
|
+
/** One `/api/desktop` `requests[]` row from a friends-pending entry (daemon or
|
|
49
|
+
* embedded). `punkId` / `avatarUrl` are null when the requester sent none —
|
|
50
|
+
* the same convention as `peers[]` — and the card then falls back to the
|
|
51
|
+
* beagles.eth lookup and a generated picture. */
|
|
52
|
+
export function desktopRequestRow(p, i, fmtTime) {
|
|
53
|
+
const a = requesterAvatar(p);
|
|
54
|
+
return {
|
|
55
|
+
id: p.userid || p.address || `r${i}`,
|
|
56
|
+
carrier: p.address || p.userid || "",
|
|
57
|
+
userid: p.userid || "",
|
|
58
|
+
// Name and introduction travel in the friend-request packet and were
|
|
59
|
+
// being dropped here, so every request looked identical and there
|
|
60
|
+
// was nothing to decide on.
|
|
61
|
+
name: p.name || "",
|
|
62
|
+
descr: p.descr || "",
|
|
63
|
+
via: p.hello || "lan",
|
|
64
|
+
time: fmtTime(p.arrivedAt),
|
|
65
|
+
// The requester's avatar, drawn before accept: URL first, then punk.
|
|
66
|
+
punkId: a.punkId ?? null,
|
|
67
|
+
avatarUrl: a.avatarUrl ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -26,6 +26,7 @@ import { bridgeRoster } from "./bridge-roster.js";
|
|
|
26
26
|
import { isSupervised } from "./relaunch.js";
|
|
27
27
|
import { historyStats } from "./assistant-stats.js";
|
|
28
28
|
import { readFriendStore, pickPunk } from "./friend-store.js";
|
|
29
|
+
import { desktopRequestRow } from "./pending-requests.js";
|
|
29
30
|
import { createDiscoverFetch } from "./discover-cache.js";
|
|
30
31
|
import { applyWalletBind } from "@decentnetwork/beagle-ui/wallet-proof";
|
|
31
32
|
// Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
|
|
@@ -2093,18 +2094,9 @@ export function startBeagleServer(opts) {
|
|
|
2093
2094
|
};
|
|
2094
2095
|
});
|
|
2095
2096
|
const pend = pending.ok ? (pending.data?.pending ?? []) : [];
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
userid: p.userid || "",
|
|
2100
|
-
// Name and introduction travel in the friend-request packet and were
|
|
2101
|
-
// being dropped here, so every request looked identical and there
|
|
2102
|
-
// was nothing to decide on.
|
|
2103
|
-
name: p.name || "",
|
|
2104
|
-
descr: p.descr || "",
|
|
2105
|
-
via: p.hello || "lan",
|
|
2106
|
-
time: fmtTime(p.arrivedAt),
|
|
2107
|
-
}));
|
|
2097
|
+
// Same rows for both backends: lan >= 0.1.313 and the embedded host
|
|
2098
|
+
// both answer friends-pending with punkId / avatarUrl when known.
|
|
2099
|
+
const requests = pend.map((p, i) => desktopRequestRow(p, i, fmtTime));
|
|
2108
2100
|
// Exit nodes = the AVAILABLE exits this node knows about, grouped by
|
|
2109
2101
|
// region. The authoritative list is the shipped DEFAULT_EXITS
|
|
2110
2102
|
// (config/default-exits.yaml) — every install has it, so the panel is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.123",
|
|
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",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"start": "node dist/cli.js"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@decentnetwork/beagle-ui": "^0.2.
|
|
29
|
+
"@decentnetwork/beagle-ui": "^0.2.34",
|
|
30
30
|
"@decentnetwork/chat-components": "^0.1.3",
|
|
31
|
-
"@decentnetwork/lan": "^0.1.
|
|
32
|
-
"@decentnetwork/peer": "^0.1.
|
|
33
|
-
"@decentnetwork/peer-webrtc": "^0.2.
|
|
31
|
+
"@decentnetwork/lan": "^0.1.313",
|
|
32
|
+
"@decentnetwork/peer": "^0.1.170",
|
|
33
|
+
"@decentnetwork/peer-webrtc": "^0.2.17",
|
|
34
34
|
"js-yaml": "^4.1.0",
|
|
35
35
|
"ws": "^8.21.1",
|
|
36
36
|
"yargs": "^17.7.2"
|