@ethora/chat-component 26.8.1 → 26.9.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/README.md +1 -0
- package/dist/{ChatProfileModal-DZVi2WRb.js → ChatProfileModal-DfXEnzJf.js} +6 -3
- package/dist/{FilePreviewModal-BelSYN5e.js → FilePreviewModal-Cs1tij9R.js} +35 -35
- package/dist/{ManageDataModal-Bs_0SHBL.js → ManageDataModal-BBOui09n.js} +3 -3
- package/dist/{MarkdownBody-BYUK4GVM.js → MarkdownBody-DttRXyAK.js} +3 -3
- package/dist/{SideDrawer-C_nWZRwH.js → SideDrawer-DLqEOqAi.js} +35 -35
- package/dist/UserProfileModal-DEtLQxkJ.js +594 -0
- package/dist/{UserSettingsModal-J7uJfki1.js → UserSettingsModal-Ddht5fc0.js} +9 -9
- package/dist/{VideoCallOverlay-BXHNEhdX.js → VideoCallOverlay-BJMarThf.js} +2673 -2662
- package/dist/VisibilityModal-CckTnGKB.js +138 -0
- package/dist/assets/icons.d.ts +8 -0
- package/dist/components/MainComponents/AttachmentList.d.ts +5 -0
- package/dist/components/MainComponents/SealedAttachmentList.d.ts +14 -0
- package/dist/components/RoomComponents/styled/LastMessageSealed.d.ts +14 -0
- package/dist/components/styled/AudioMessage.d.ts +38 -0
- package/dist/components/styled/MessageImage.d.ts +10 -0
- package/dist/e2ee/crypto.d.ts +32 -0
- package/dist/e2ee/fileEnvelope.d.ts +48 -0
- package/dist/e2ee/index.d.ts +53 -0
- package/dist/e2ee/omemo.d.ts +130 -0
- package/dist/e2ee/protobuf.d.ts +25 -0
- package/dist/e2ee/ratchet.d.ts +54 -0
- package/dist/e2ee/stanza.d.ts +24 -0
- package/dist/e2ee/store.d.ts +12 -0
- package/dist/{handleCopyClick-Bj2V_brV.js → handleCopyClick-CWw4UAlT.js} +2 -2
- package/dist/helpers/findMessagesForFile.d.ts +25 -0
- package/dist/helpers/sealedAttachments.d.ts +25 -0
- package/dist/helpers/xmppIdShape.d.ts +9 -0
- package/dist/helpers/xmppUsername.d.ts +1 -1
- package/dist/hooks/useChatWrapperInit.d.ts +5 -1
- package/dist/{main-DmtEeYji.js → main-BHtKhpBZ.js} +21004 -16820
- package/dist/main.d.ts +2 -0
- package/dist/main.js +23 -20
- package/dist/networking/api-requests/auth.api.d.ts +10 -1
- package/dist/networking/api-requests/rooms.api.d.ts +1 -1
- package/dist/networking/authRefresh.d.ts +6 -0
- package/dist/networking/xmpp/sendMediaMessage.xmpp.d.ts +12 -1
- package/dist/networking/xmpp/sendTextMessage.xmpp.d.ts +1 -1
- package/dist/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.d.ts +1 -1
- package/dist/networking/xmppClient.d.ts +39 -0
- package/dist/roomStore/storage.d.ts +2 -1
- package/dist/styles/tokens.d.ts +14 -4
- package/dist/types/models/config.model.d.ts +33 -0
- package/dist/types/models/message.model.d.ts +27 -0
- package/dist/types/models/room.model.d.ts +10 -0
- package/package.json +4 -1
- package/dist/UserProfileModal-CD2IB5SD.js +0 -574
- package/dist/VisibilityModal-4QJnWpAL.js +0 -132
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { jsxs as r, jsx as s } from "react/jsx-runtime";
|
|
2
|
+
import { useState as b, useEffect as x } from "react";
|
|
3
|
+
import { x as o, a as L, p as P, u as z, aG as D, aH as I } from "./main-BHtKhpBZ.js";
|
|
4
|
+
import { S as N, D as C, a as $, b as k, e as w, f as p } from "./SideDrawer-DLqEOqAi.js";
|
|
5
|
+
o.div``;
|
|
6
|
+
o.div`
|
|
7
|
+
font-size: 16px;
|
|
8
|
+
font-weight: bold;
|
|
9
|
+
color: var(--ethora-color-text, #141414);
|
|
10
|
+
`;
|
|
11
|
+
o.p`
|
|
12
|
+
font-size: 14px;
|
|
13
|
+
color: var(--ethora-color-text-muted, #6b7280);
|
|
14
|
+
margin-bottom: 32px;
|
|
15
|
+
`;
|
|
16
|
+
const O = o.div`
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: 6px;
|
|
20
|
+
`, h = o.label`
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
`, u = o.input`
|
|
26
|
+
accent-color: ${({ $radioColor: i }) => i || "var(--ethora-color-primary, #0052CD)"};
|
|
27
|
+
margin: 0px;
|
|
28
|
+
`, R = o.div`
|
|
29
|
+
padding: 10px 15px;
|
|
30
|
+
margin-top: 16px;
|
|
31
|
+
color: ${({ type: i }) => i === "success" ? "#10b981" : "#ef4444"};
|
|
32
|
+
background: ${({ type: i }) => i === "success" ? "#d1fae5" : "#fee2e2"};
|
|
33
|
+
border: 1px solid
|
|
34
|
+
${({ type: i }) => i === "success" ? "#10b981" : "#ef4444"};
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
|
|
37
|
+
[data-ethora-color-scheme='dark'] & {
|
|
38
|
+
color: ${({ type: i }) => i === "success" ? "var(--ethora-color-success, #10b981)" : "var(--ethora-color-danger, #ef4444)"};
|
|
39
|
+
background: ${({ type: i }) => i === "success" ? "rgba(16, 185, 129, 0.15)" : "rgba(242, 118, 107, 0.15)"};
|
|
40
|
+
border-color: ${({ type: i }) => i === "success" ? "var(--ethora-color-success, #10b981)" : "var(--ethora-color-danger, #ef4444)"};
|
|
41
|
+
}
|
|
42
|
+
`, H = ({
|
|
43
|
+
handleCloseModal: i
|
|
44
|
+
}) => {
|
|
45
|
+
const S = L(), e = P(), { user: n, config: l } = z(
|
|
46
|
+
(t) => t.chatSettingStore
|
|
47
|
+
), g = (t) => S(I(t)), [a, m] = b(n?.isProfileOpen), [c, v] = b(n?.isAssetsOpen), [f, y] = b(null), d = (t, A) => {
|
|
48
|
+
y({ message: t, type: A }), setTimeout(() => y(null), 3e3);
|
|
49
|
+
};
|
|
50
|
+
return x(() => {
|
|
51
|
+
a !== n.isProfileOpen && D({ isProfileOpen: a }).then(({ data: t }) => {
|
|
52
|
+
g(t.user), d(e("notification.saved"), "success");
|
|
53
|
+
}).catch(() => d(e("toast.error"), "error"));
|
|
54
|
+
}, [a]), x(() => {
|
|
55
|
+
c !== n?.isAssetsOpen && D({ isAssetsOpen: c }).then(({ data: t }) => {
|
|
56
|
+
g(t.user), d(e("notification.saved"), "success");
|
|
57
|
+
}).catch(() => d(e("toast.error"), "error"));
|
|
58
|
+
}, [c]), /* @__PURE__ */ r(
|
|
59
|
+
N,
|
|
60
|
+
{
|
|
61
|
+
title: e("settings.visibility.title"),
|
|
62
|
+
onClose: i,
|
|
63
|
+
backLabel: e("action.back"),
|
|
64
|
+
children: [
|
|
65
|
+
/* @__PURE__ */ r(C, { children: [
|
|
66
|
+
/* @__PURE__ */ s($, { children: e("settings.visibility.profileLabel") }),
|
|
67
|
+
/* @__PURE__ */ s(k, { children: /* @__PURE__ */ s(w, { children: /* @__PURE__ */ r(O, { children: [
|
|
68
|
+
/* @__PURE__ */ r(h, { children: [
|
|
69
|
+
/* @__PURE__ */ s(
|
|
70
|
+
u,
|
|
71
|
+
{
|
|
72
|
+
$radioColor: l?.colors?.primary,
|
|
73
|
+
type: "radio",
|
|
74
|
+
name: "ethora-profile-visibility",
|
|
75
|
+
checked: a === !0,
|
|
76
|
+
onChange: () => m(!0)
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
e("settings.visibility.open")
|
|
80
|
+
] }),
|
|
81
|
+
/* @__PURE__ */ s(p, { children: e("settings.visibility.openDescription") }),
|
|
82
|
+
/* @__PURE__ */ r(h, { children: [
|
|
83
|
+
/* @__PURE__ */ s(
|
|
84
|
+
u,
|
|
85
|
+
{
|
|
86
|
+
$radioColor: l?.colors?.primary,
|
|
87
|
+
type: "radio",
|
|
88
|
+
name: "ethora-profile-visibility",
|
|
89
|
+
checked: a === !1,
|
|
90
|
+
onChange: () => m(!1)
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
e("settings.visibility.restricted")
|
|
94
|
+
] }),
|
|
95
|
+
/* @__PURE__ */ s(p, { children: e("settings.visibility.restrictedDescription") })
|
|
96
|
+
] }) }) })
|
|
97
|
+
] }),
|
|
98
|
+
/* @__PURE__ */ r(C, { children: [
|
|
99
|
+
/* @__PURE__ */ s($, { children: e("settings.visibility.documentsLabel") }),
|
|
100
|
+
/* @__PURE__ */ s(k, { children: /* @__PURE__ */ s(w, { children: /* @__PURE__ */ r(O, { children: [
|
|
101
|
+
/* @__PURE__ */ r(h, { children: [
|
|
102
|
+
/* @__PURE__ */ s(
|
|
103
|
+
u,
|
|
104
|
+
{
|
|
105
|
+
$radioColor: l?.colors?.primary,
|
|
106
|
+
type: "radio",
|
|
107
|
+
name: "ethora-documents-visibility",
|
|
108
|
+
checked: c === !0,
|
|
109
|
+
onChange: () => v(!0)
|
|
110
|
+
}
|
|
111
|
+
),
|
|
112
|
+
e("settings.visibility.full")
|
|
113
|
+
] }),
|
|
114
|
+
/* @__PURE__ */ s(p, { children: e("settings.visibility.fullDescription") }),
|
|
115
|
+
/* @__PURE__ */ r(h, { children: [
|
|
116
|
+
/* @__PURE__ */ s(
|
|
117
|
+
u,
|
|
118
|
+
{
|
|
119
|
+
$radioColor: l?.colors?.primary,
|
|
120
|
+
type: "radio",
|
|
121
|
+
name: "ethora-documents-visibility",
|
|
122
|
+
checked: c === !1,
|
|
123
|
+
onChange: () => v(!1)
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
e("settings.visibility.individual")
|
|
127
|
+
] }),
|
|
128
|
+
/* @__PURE__ */ s(p, { children: e("settings.visibility.individualDescription") })
|
|
129
|
+
] }) }) })
|
|
130
|
+
] }),
|
|
131
|
+
f && /* @__PURE__ */ s(R, { type: f.type, children: f.message })
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
export {
|
|
137
|
+
H as default
|
|
138
|
+
};
|
package/dist/assets/icons.d.ts
CHANGED
|
@@ -172,3 +172,11 @@ export declare const ZoomResetIcon: ({ color, ...props }: {
|
|
|
172
172
|
[x: string]: any;
|
|
173
173
|
color?: string;
|
|
174
174
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
175
|
+
export declare const LockIcon: ({ color, ...props }: {
|
|
176
|
+
[x: string]: any;
|
|
177
|
+
color?: string;
|
|
178
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
179
|
+
export declare const LockOffIcon: ({ color, ...props }: {
|
|
180
|
+
[x: string]: any;
|
|
181
|
+
color?: string;
|
|
182
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,6 +2,11 @@ import { default as React } from 'react';
|
|
|
2
2
|
import { IAttachment } from '../../types/types';
|
|
3
3
|
interface AttachmentListProps {
|
|
4
4
|
attachments: IAttachment[];
|
|
5
|
+
/** See CustomMessageImage's `onUnavailable` - only wired to the full-width
|
|
6
|
+
* single-image renderer (AttachmentRow), not the multi-file grid tiles,
|
|
7
|
+
* since one dead thumbnail there shouldn't tombstone a message that still
|
|
8
|
+
* has other live attachments. */
|
|
9
|
+
onUnavailable?: () => void;
|
|
5
10
|
}
|
|
6
11
|
declare const AttachmentList: React.FC<AttachmentListProps>;
|
|
7
12
|
export default AttachmentList;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { IAttachment } from '../../types/types';
|
|
3
|
+
/**
|
|
4
|
+
* Sealed attachments, offered as downloads rather than previews.
|
|
5
|
+
*
|
|
6
|
+
* `keys` are positional: keys[i] opens attachments[i] (getDataFromXml lifts
|
|
7
|
+
* them out of the OMEMO-decrypted <body>). An attachment with no key belongs
|
|
8
|
+
* to a message we could not decrypt, and renders as unavailable.
|
|
9
|
+
*/
|
|
10
|
+
declare const SealedAttachmentList: React.FC<{
|
|
11
|
+
attachments: IAttachment[];
|
|
12
|
+
keys?: string[];
|
|
13
|
+
}>;
|
|
14
|
+
export default SealedAttachmentList;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { LastMessage } from '../../../types/types';
|
|
3
|
+
interface LastMessageSealedProps extends Pick<LastMessage, 'user'> {
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Room-list preview for a sealed attachment.
|
|
7
|
+
*
|
|
8
|
+
* It cannot say what the file is: the name and type are inside the seal and
|
|
9
|
+
* only come out on download, and the server-held ones are placeholders - an
|
|
10
|
+
* opaque `originalName` and `application/octet-stream`. So it says exactly
|
|
11
|
+
* what is known, which is that an encrypted file arrived.
|
|
12
|
+
*/
|
|
13
|
+
declare const LastMessageSealed: FC<LastMessageSealedProps>;
|
|
14
|
+
export default LastMessageSealed;
|
|
@@ -2,5 +2,43 @@ import { default as React } from 'react';
|
|
|
2
2
|
interface AudioMessageProps {
|
|
3
3
|
src: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* wavesurfer paints into a canvas, and a canvas fill takes a concrete
|
|
7
|
+
* colour - it cannot read `var(--ethora-color-primary)` or `color-mix()`
|
|
8
|
+
* portably. So the colours are resolved off the DOM and mixed here.
|
|
9
|
+
*/
|
|
10
|
+
type Rgb = {
|
|
11
|
+
r: number;
|
|
12
|
+
g: number;
|
|
13
|
+
b: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Played and unplayed bar colours for whatever this bubble turned out to
|
|
17
|
+
* be. The brand colour leads when it can be seen against the bubble; when
|
|
18
|
+
* it cannot (own-message bubbles, and any host whose primary is close to
|
|
19
|
+
* its own surface) the bubble's text colour takes over, which is readable
|
|
20
|
+
* on that surface by definition. Unplayed bars are the same colour mixed
|
|
21
|
+
* halfway into the surface, so they read as "the rest of the clip" rather
|
|
22
|
+
* than as a different element.
|
|
23
|
+
*/
|
|
24
|
+
export declare const resolveWaveColors: (primary: string, text: string, surface: Rgb) => {
|
|
25
|
+
wave: string;
|
|
26
|
+
progress: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Bars for one voice note.
|
|
30
|
+
*
|
|
31
|
+
* NOT the peak envelope wavesurfer draws by itself. Measured on a real QA
|
|
32
|
+
* recording: two transients hit 0.98 and 0.99 while the speech around them
|
|
33
|
+
* sits at an RMS of 0.002 to 0.058. Scaled against the peak, those two
|
|
34
|
+
* samples own the whole 28px and every other bar collapses to a hairline -
|
|
35
|
+
* the "faint dashed line with a couple of spikes" look these bubbles had.
|
|
36
|
+
*
|
|
37
|
+
* So: RMS per bucket (what the ear hears, not the worst sample), scaled
|
|
38
|
+
* against the 95th percentile so a single click cannot set the scale, then
|
|
39
|
+
* a square-root curve because loudness is perceived that way, and a floor
|
|
40
|
+
* so a quiet passage still reads as a bar rather than as nothing.
|
|
41
|
+
*/
|
|
42
|
+
export declare const buildWaveformPeaks: (samples: Float32Array, barCount: number) => number[];
|
|
5
43
|
declare const AudioMessage: React.FC<AudioMessageProps>;
|
|
6
44
|
export default AudioMessage;
|
|
@@ -4,6 +4,16 @@ interface CustomMessageImageProps {
|
|
|
4
4
|
fileName: string;
|
|
5
5
|
mimetype: string;
|
|
6
6
|
locationPreview?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Called only once the server itself says the file is gone (404/410),
|
|
9
|
+
* not merely when the <img> failed to load. The only known cause in
|
|
10
|
+
* practice is the file having been deleted from the Files panel without
|
|
11
|
+
* a matching message found locally to tombstone up front (see
|
|
12
|
+
* FilesPanel.tsx's handleDelete) - the caller uses this to flip the
|
|
13
|
+
* message to the normal "deleted" placeholder instead of leaving this
|
|
14
|
+
* broken-image card on screen.
|
|
15
|
+
*/
|
|
16
|
+
onUnavailable?: () => void;
|
|
7
17
|
}
|
|
8
18
|
declare const CustomMessageImage: React.FC<CustomMessageImageProps>;
|
|
9
19
|
export default CustomMessageImage;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { concatBytes, randomBytes } from '@noble/hashes/utils.js';
|
|
2
|
+
export { concatBytes, randomBytes };
|
|
3
|
+
export interface KeyPair {
|
|
4
|
+
priv: Uint8Array;
|
|
5
|
+
pub: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
/** Ed25519 identity key; `seed` is the 32-byte Ed25519 secret key. */
|
|
8
|
+
export interface IdentityKey {
|
|
9
|
+
seed: Uint8Array;
|
|
10
|
+
pub: Uint8Array;
|
|
11
|
+
}
|
|
12
|
+
export declare function hkdfSha256(ikm: Uint8Array, salt: Uint8Array | undefined, info: string, length: number): Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>;
|
|
13
|
+
export declare function hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array;
|
|
14
|
+
/** AES-256-CBC with PKCS#7 padding. */
|
|
15
|
+
export declare function aesEncrypt(key: Uint8Array, iv: Uint8Array, data: Uint8Array): Uint8Array;
|
|
16
|
+
export declare function aesDecrypt(key: Uint8Array, iv: Uint8Array, data: Uint8Array): Uint8Array;
|
|
17
|
+
export declare function generateKeyPair(): KeyPair;
|
|
18
|
+
export declare function generateIdentity(): IdentityKey;
|
|
19
|
+
/** X25519; throws on low-order public keys. */
|
|
20
|
+
export declare function dh(priv: Uint8Array, pub: Uint8Array): Uint8Array;
|
|
21
|
+
/** The X25519 form of an identity key, for X3DH. */
|
|
22
|
+
export declare function identityDhPrivate(identity: IdentityKey): Uint8Array;
|
|
23
|
+
export declare function identityDhPublic(identityPub: Uint8Array): Uint8Array;
|
|
24
|
+
export declare function sign(identity: IdentityKey, message: Uint8Array): Uint8Array;
|
|
25
|
+
export declare function verify(signature: Uint8Array, message: Uint8Array, identityPub: Uint8Array): boolean;
|
|
26
|
+
/** Constant-time comparison. */
|
|
27
|
+
export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
28
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
29
|
+
export declare function fromBase64(text: string): Uint8Array;
|
|
30
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
31
|
+
/** Random integer in [min, max]. */
|
|
32
|
+
export declare function randomInt(min: number, max: number): number;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** What the server must not learn, and the receiver needs to render. */
|
|
2
|
+
export interface FileEnvelopeMeta {
|
|
3
|
+
mimetype: string;
|
|
4
|
+
originalname: string;
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SealedFile {
|
|
8
|
+
/** Ciphertext, ready to upload. */
|
|
9
|
+
ciphertext: Uint8Array;
|
|
10
|
+
/** Base64 of the 48-byte key material. Belongs in the encrypted <body>. */
|
|
11
|
+
keyMaterial: string;
|
|
12
|
+
/** Random, extension-free name to upload under. */
|
|
13
|
+
filename: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* MAGIC(8) | version(1) | headerLen(4, big-endian) | headerJSON | fileBytes
|
|
17
|
+
*
|
|
18
|
+
* Length-prefixed rather than delimited: a JSON header is arbitrary text and
|
|
19
|
+
* the body is arbitrary bytes, so there is no separator that cannot occur in
|
|
20
|
+
* either.
|
|
21
|
+
*/
|
|
22
|
+
export declare function encodeFileEnvelope(meta: FileEnvelopeMeta, bytes: Uint8Array): Uint8Array;
|
|
23
|
+
export declare function decodeFileEnvelope(envelope: Uint8Array): {
|
|
24
|
+
meta: FileEnvelopeMeta;
|
|
25
|
+
bytes: Uint8Array;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Seal a file for upload.
|
|
29
|
+
*
|
|
30
|
+
* Reuses the OMEMO payload primitives (AES-256-CBC + truncated HMAC, fresh
|
|
31
|
+
* random key per call) so attachments and message bodies are protected by the
|
|
32
|
+
* same construction rather than a second, separately-reviewed one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sealFileForUpload(file: File | Blob): Promise<SealedFile>;
|
|
35
|
+
/** Inverse of sealFileForUpload, for the receiving side. */
|
|
36
|
+
export declare function openSealedFile(ciphertext: Uint8Array, keyMaterial: string): {
|
|
37
|
+
meta: FileEnvelopeMeta;
|
|
38
|
+
bytes: Uint8Array;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Pull the attachment keys out of a decrypted media <body>.
|
|
42
|
+
*
|
|
43
|
+
* The sender put `{v:1,keys:[...]}` there instead of the usual literal
|
|
44
|
+
* "media" (see sendMediaMessage). Returns undefined for anything else, so
|
|
45
|
+
* an ordinary media message - or one from a build that predates sealing -
|
|
46
|
+
* falls through untouched.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseSealedMediaBody(body?: string): string[] | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Client } from '@xmpp/client';
|
|
2
|
+
import { Omemo } from './omemo';
|
|
3
|
+
export { NS_OMEMO, FALLBACK_BODY, formatFingerprint } from './omemo';
|
|
4
|
+
export type { Decrypted, DeviceInfo, Trust } from './omemo';
|
|
5
|
+
export { Omemo } from './omemo';
|
|
6
|
+
/**
|
|
7
|
+
* Set from IConfig when the chat mounts.
|
|
8
|
+
*
|
|
9
|
+
* The config effect and the XMPP `online` event race, and either order is
|
|
10
|
+
* normal, so both this and onOnline() end in the same maybeStart().
|
|
11
|
+
*/
|
|
12
|
+
export declare function setE2eeEnabled(value: boolean): void;
|
|
13
|
+
export declare function isE2eeEnabled(): boolean;
|
|
14
|
+
/** Called once the XMPP session is up. */
|
|
15
|
+
export declare function onOnline(client: Client): void;
|
|
16
|
+
/** Drops the in-memory instance. Stored keys survive, so a re-login resumes. */
|
|
17
|
+
export declare function stopOmemo(): void;
|
|
18
|
+
/** The live instance, or undefined when encryption is off / not ready. */
|
|
19
|
+
export declare function omemo(): Omemo | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The instance once it is usable, rather than whatever exists right now.
|
|
22
|
+
*
|
|
23
|
+
* Publishing this device's bundle takes a round trip, and the server starts
|
|
24
|
+
* pushing archived messages the moment we join a room - so the first page of
|
|
25
|
+
* history routinely arrives while OMEMO is still starting. Reading `omemo()`
|
|
26
|
+
* there returns undefined and those messages get permanently rendered as
|
|
27
|
+
* undecryptable, even though the keys to read them land a moment later.
|
|
28
|
+
* Awaiting this instead makes the seam wait out that window.
|
|
29
|
+
*/
|
|
30
|
+
export declare function omemoReady(): Promise<Omemo | undefined>;
|
|
31
|
+
/** True when the backend marked this room as end-to-end encrypted. */
|
|
32
|
+
export declare function isE2eeRoom(roomJid: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Bare JIDs to encrypt for: every member of the room.
|
|
35
|
+
*
|
|
36
|
+
* The member list comes from `GET /v1/chats/my` (RoomMember.xmppUsername), so
|
|
37
|
+
* it is the backend's view of who belongs to the room, not the list of who
|
|
38
|
+
* happens to be joined right now. That is what we want - somebody offline
|
|
39
|
+
* still has to be able to read the message later.
|
|
40
|
+
*/
|
|
41
|
+
export declare function roomRecipients(roomJid: string, domain: string): string[];
|
|
42
|
+
/**
|
|
43
|
+
* The real bare JID behind a MUC `from`.
|
|
44
|
+
*
|
|
45
|
+
* `room@conference.host/nick` -> `nick@host`. This works because the SDK
|
|
46
|
+
* always joins with `client.jid.getLocal()` as the nick (presenceInRoom and
|
|
47
|
+
* createRoomPresence), so the nick IS the account's localpart. It is also why
|
|
48
|
+
* OMEMO works here without making rooms non-anonymous: we never need the MUC
|
|
49
|
+
* to disclose real JIDs.
|
|
50
|
+
*/
|
|
51
|
+
export declare function senderJidFromMuc(from: string | undefined, domain: string): string | undefined;
|
|
52
|
+
/** Account domain taken from our own JID, e.g. "dappros.com". */
|
|
53
|
+
export declare function accountDomain(client: Client | undefined): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Client } from '@xmpp/client';
|
|
2
|
+
import { Element } from 'ltx';
|
|
3
|
+
import { OmemoStore } from './store';
|
|
4
|
+
export declare const NS_OMEMO = "urn:xmpp:omemo:2";
|
|
5
|
+
/**
|
|
6
|
+
* The cleartext <body> of an encrypted stanza.
|
|
7
|
+
*
|
|
8
|
+
* Two audiences, one string, and it cannot be localised for either: it is
|
|
9
|
+
* written once by the sender, who does not know the reader's language.
|
|
10
|
+
*
|
|
11
|
+
* - clients that cannot read `urn:xmpp:omemo:2` render it in place of the
|
|
12
|
+
* message;
|
|
13
|
+
* - the server's push module copies it into `msgText`, so it is what lands on
|
|
14
|
+
* a lock screen. That rules out anything phrased as an error - keep it to a
|
|
15
|
+
* short, neutral label.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FALLBACK_BODY = "Encrypted message";
|
|
18
|
+
/** Blind Trust Before Verification: new devices are trusted until one is verified. */
|
|
19
|
+
export type Trust = 'blind' | 'verified' | 'untrusted';
|
|
20
|
+
export interface Decrypted {
|
|
21
|
+
/**
|
|
22
|
+
* Content of the SCE envelope: the original <body>, <data> and whatever
|
|
23
|
+
* else the sender put there, serialized back to XML. Undefined for empty
|
|
24
|
+
* messages, which exist only to advance a session.
|
|
25
|
+
*/
|
|
26
|
+
content?: string;
|
|
27
|
+
/** 'own' - sent from this device, read back from the local cache. */
|
|
28
|
+
trust: Trust | 'own';
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DeviceInfo {
|
|
32
|
+
id: number;
|
|
33
|
+
fingerprint: string;
|
|
34
|
+
trust: Trust | 'own';
|
|
35
|
+
}
|
|
36
|
+
/** Identity key fingerprint as shown to users: hex in groups of 8. */
|
|
37
|
+
export declare function formatFingerprint(ik: Uint8Array): string;
|
|
38
|
+
/** OMEMO 2 for one account on this device, across the MUC rooms it is in. */
|
|
39
|
+
export declare class Omemo {
|
|
40
|
+
/** Our own bare JID. */
|
|
41
|
+
readonly jid: string;
|
|
42
|
+
private readonly xmpp;
|
|
43
|
+
private readonly store;
|
|
44
|
+
private keys;
|
|
45
|
+
private queue;
|
|
46
|
+
private deviceLists;
|
|
47
|
+
private constructor();
|
|
48
|
+
/** Loads (or creates) this device's keys and publishes them. */
|
|
49
|
+
static create(xmpp: Client, jid: string, store: OmemoStore): Promise<Omemo>;
|
|
50
|
+
get deviceId(): number;
|
|
51
|
+
get fingerprint(): string;
|
|
52
|
+
/** Runs tasks one at a time: sessions and keys are read-modify-write. */
|
|
53
|
+
private serial;
|
|
54
|
+
/**
|
|
55
|
+
* Publishes, and repairs the node if its configuration blocks us.
|
|
56
|
+
*
|
|
57
|
+
* Without this, one server-side change to the PEP node configuration locks
|
|
58
|
+
* every existing account out of publishing its keys for good: the device
|
|
59
|
+
* can never announce itself, so nobody can encrypt to it and everything it
|
|
60
|
+
* sends silently degrades to plaintext. Reconfiguring is tried first
|
|
61
|
+
* because it keeps the published items; deleting the node is the last
|
|
62
|
+
* resort, and costs only bundles that every device republishes on its next
|
|
63
|
+
* login anyway.
|
|
64
|
+
*/
|
|
65
|
+
private publishWithRecovery;
|
|
66
|
+
/** Rewrites an existing node's configuration to what we publish against. */
|
|
67
|
+
private configureNode;
|
|
68
|
+
private deleteNode;
|
|
69
|
+
private publish;
|
|
70
|
+
private fetchItems;
|
|
71
|
+
private fetchDeviceList;
|
|
72
|
+
private deviceIds;
|
|
73
|
+
/** Adds this device to our published device list. */
|
|
74
|
+
private announceDevice;
|
|
75
|
+
private publishBundle;
|
|
76
|
+
private fetchBundle;
|
|
77
|
+
/** Records the device's identity key on first sight and returns its trust. */
|
|
78
|
+
private trustOf;
|
|
79
|
+
/** Devices of `jid` (fetching unknown bundles), including this one for our own JID. */
|
|
80
|
+
devices(jid: string): Promise<DeviceInfo[]>;
|
|
81
|
+
setTrust(jid: string, id: number, trust: Trust): Promise<void>;
|
|
82
|
+
/** Encrypts `keyMaterial` for every trusted device of `jid` (except this one). */
|
|
83
|
+
private encryptKey;
|
|
84
|
+
/**
|
|
85
|
+
* Builds the encrypted groupchat stanza for `roomJid`.
|
|
86
|
+
*
|
|
87
|
+
* The plaintext stanza's children are split in two:
|
|
88
|
+
*
|
|
89
|
+
* - `content` goes inside the SCE envelope and is what OMEMO protects.
|
|
90
|
+
* Today that is <body> alone.
|
|
91
|
+
* - `cleartext` is appended to the stanza untouched, beside <encrypted>.
|
|
92
|
+
* That is Ethora's <data> element, which the server's push module reads
|
|
93
|
+
* directly off the wire (get_data_map in mod_offline_post takes the
|
|
94
|
+
* first <data> sub-element); encrypting it turns every notification into
|
|
95
|
+
* a bare JID localpart plus FALLBACK_BODY.
|
|
96
|
+
*
|
|
97
|
+
* Nothing in `cleartext` is protected in any way - it is archived in MAM
|
|
98
|
+
* and POSTed to the push service exactly as an unencrypted room's would
|
|
99
|
+
* be. Keep it to what the server genuinely has to read, and never move a
|
|
100
|
+
* message's text into it.
|
|
101
|
+
*
|
|
102
|
+
* `members` are the bare JIDs of everyone in the room - ours is added
|
|
103
|
+
* automatically so our other devices can read it too. `id` must be the id
|
|
104
|
+
* the plaintext stanza would have used, because the optimistic UI matches
|
|
105
|
+
* the room's echo by it.
|
|
106
|
+
*/
|
|
107
|
+
encryptGroupMessage(roomJid: string, members: string[], content: Element[], id: string, cleartext?: Element[]): Promise<Element>;
|
|
108
|
+
/**
|
|
109
|
+
* Empty OMEMO message: completes a session the peer started. It targets one
|
|
110
|
+
* device, so it goes straight to that member's real JID rather than through
|
|
111
|
+
* the room.
|
|
112
|
+
*/
|
|
113
|
+
private sendEmpty;
|
|
114
|
+
/**
|
|
115
|
+
* Decrypts one OMEMO stanza. `sender` is the real bare JID of the author
|
|
116
|
+
* (resolved from the room nick by the caller) and `roomJid` the room it
|
|
117
|
+
* arrived in. Returns undefined when the stanza carries no OMEMO payload.
|
|
118
|
+
*/
|
|
119
|
+
decrypt(el: Element, sender: string, roomJid: string): Promise<Decrypted> | undefined;
|
|
120
|
+
private decryptUncached;
|
|
121
|
+
/**
|
|
122
|
+
* Checks the SCE envelope (XEP-0420) and returns its content as XML.
|
|
123
|
+
*
|
|
124
|
+
* The from/to check is what stops one member replaying another's encrypted
|
|
125
|
+
* message under their own nick: `from` must be the JID the room nick
|
|
126
|
+
* resolves to, and `to` must be the room the stanza arrived in.
|
|
127
|
+
*/
|
|
128
|
+
private openEnvelope;
|
|
129
|
+
private replacePrekey;
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface OmemoMessage {
|
|
2
|
+
n: number;
|
|
3
|
+
pn: number;
|
|
4
|
+
dhPub: Uint8Array;
|
|
5
|
+
ciphertext?: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
export declare function encodeMessage(m: OmemoMessage): Uint8Array;
|
|
8
|
+
export declare function decodeMessage(buf: Uint8Array): OmemoMessage;
|
|
9
|
+
export interface AuthenticatedMessage {
|
|
10
|
+
mac: Uint8Array;
|
|
11
|
+
/** Encoded OMEMOMessage. */
|
|
12
|
+
message: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
export declare function encodeAuthenticated(m: AuthenticatedMessage): Uint8Array;
|
|
15
|
+
export declare function decodeAuthenticated(buf: Uint8Array): AuthenticatedMessage;
|
|
16
|
+
export interface KeyExchange {
|
|
17
|
+
pkId: number;
|
|
18
|
+
spkId: number;
|
|
19
|
+
ik: Uint8Array;
|
|
20
|
+
ek: Uint8Array;
|
|
21
|
+
/** Encoded OMEMOAuthenticatedMessage. */
|
|
22
|
+
message: Uint8Array;
|
|
23
|
+
}
|
|
24
|
+
export declare function encodeKeyExchange(m: KeyExchange): Uint8Array;
|
|
25
|
+
export declare function decodeKeyExchange(buf: Uint8Array): KeyExchange;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { IdentityKey, KeyPair } from './crypto';
|
|
2
|
+
export interface Bundle {
|
|
3
|
+
ik: Uint8Array;
|
|
4
|
+
spkId: number;
|
|
5
|
+
spk: Uint8Array;
|
|
6
|
+
spkSignature: Uint8Array;
|
|
7
|
+
pkId: number;
|
|
8
|
+
pk: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
export interface Session {
|
|
11
|
+
rk: Uint8Array;
|
|
12
|
+
dhs: KeyPair;
|
|
13
|
+
dhr?: Uint8Array;
|
|
14
|
+
cks?: Uint8Array;
|
|
15
|
+
ckr?: Uint8Array;
|
|
16
|
+
ns: number;
|
|
17
|
+
nr: number;
|
|
18
|
+
pn: number;
|
|
19
|
+
/** Message keys of skipped messages, by `${dhPub}:${n}`, oldest first. */
|
|
20
|
+
skipped: Record<string, Uint8Array>;
|
|
21
|
+
/** X3DH associated data: initiator IK || responder IK. */
|
|
22
|
+
ad: Uint8Array;
|
|
23
|
+
/** The peer device's Ed25519 identity key. */
|
|
24
|
+
peerIk: Uint8Array;
|
|
25
|
+
/** We initiated and got no reply yet: wrap outgoing messages in a key exchange. */
|
|
26
|
+
pendingKex?: {
|
|
27
|
+
pkId: number;
|
|
28
|
+
spkId: number;
|
|
29
|
+
ek: Uint8Array;
|
|
30
|
+
};
|
|
31
|
+
/** Ephemeral key of the key exchange that created this session (responder side). */
|
|
32
|
+
kexEk?: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
/** Starts a session with a device from its published bundle (X3DH, active side). */
|
|
35
|
+
export declare function initiateSession(own: IdentityKey, bundle: Bundle): Session;
|
|
36
|
+
/** Builds the session from a received key exchange (X3DH, passive side). */
|
|
37
|
+
export declare function acceptSession(own: IdentityKey, spk: KeyPair, pk: KeyPair, peerIk: Uint8Array, ek: Uint8Array): Session;
|
|
38
|
+
/** Encrypts with the sending chain; returns an encoded OMEMOAuthenticatedMessage. */
|
|
39
|
+
export declare function ratchetEncrypt(session: Session, plaintext: Uint8Array): [Session, Uint8Array];
|
|
40
|
+
/**
|
|
41
|
+
* Decrypts an encoded OMEMOAuthenticatedMessage. The input session is left
|
|
42
|
+
* untouched; the returned one must be stored only after the whole message
|
|
43
|
+
* was processed successfully.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ratchetDecrypt(session: Session, data: Uint8Array): [Session, Uint8Array];
|
|
46
|
+
/**
|
|
47
|
+
* Encrypts the SCE envelope. `keyMaterial` (key || truncated HMAC, 48 bytes)
|
|
48
|
+
* is what gets encrypted for every recipient device.
|
|
49
|
+
*/
|
|
50
|
+
export declare function encryptPayload(plaintext: Uint8Array): {
|
|
51
|
+
keyMaterial: Uint8Array;
|
|
52
|
+
payload: Uint8Array;
|
|
53
|
+
};
|
|
54
|
+
export declare function decryptPayload(keyMaterial: Uint8Array, payload: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Element } from 'ltx';
|
|
2
|
+
export type DecryptOutcome =
|
|
3
|
+
/** No OMEMO payload; the caller should carry on as before. */
|
|
4
|
+
'not-encrypted'
|
|
5
|
+
/** Rewritten with the sender's original children. */
|
|
6
|
+
| 'decrypted'
|
|
7
|
+
/** Rewritten with a placeholder body; still a message worth showing. */
|
|
8
|
+
| 'undecryptable'
|
|
9
|
+
/** Session maintenance only - there is nothing to render, skip it. */
|
|
10
|
+
| 'drop';
|
|
11
|
+
/**
|
|
12
|
+
* Finds the <message> that actually carries <encrypted>, looking through the
|
|
13
|
+
* wrappers a message can arrive in: a mucsub event, a MAM <result>/<forwarded>
|
|
14
|
+
* pair, or both at once.
|
|
15
|
+
*/
|
|
16
|
+
export declare function encryptedCarrier(stanza: Element, depth?: number): Element | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Decrypts `stanza` in place, if it carries OMEMO at all.
|
|
19
|
+
*
|
|
20
|
+
* `stanza` may be the message itself or anything wrapping it. Safe to call on
|
|
21
|
+
* every message: without an OMEMO payload it returns immediately and touches
|
|
22
|
+
* nothing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function decryptStanzaInPlace(stanza: Element, domain: string): Promise<DecryptOutcome>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Key-value storage for OMEMO keys, sessions and decrypted messages. */
|
|
2
|
+
export interface OmemoStore {
|
|
3
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
4
|
+
set(key: string, value: unknown): Promise<void>;
|
|
5
|
+
delete(key: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* IndexedDB database per account. Private keys are stored unencrypted, like
|
|
9
|
+
* in other web clients: anyone with access to this browser profile can read them.
|
|
10
|
+
*/
|
|
11
|
+
export declare function openIndexedDbStore(jid: string): Promise<OmemoStore>;
|
|
12
|
+
export declare function createMemoryStore(): OmemoStore;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { x as e, z as t } from "./main-BHtKhpBZ.js";
|
|
2
2
|
const i = e.div`
|
|
3
3
|
display: flex;
|
|
4
4
|
flex-direction: column;
|
|
@@ -59,7 +59,7 @@ const i = e.div`
|
|
|
59
59
|
padding: var(--ethora-space-1, 4px) var(--ethora-space-3, 12px);
|
|
60
60
|
border-radius: var(--ethora-radius-full, 999px);
|
|
61
61
|
background: var(--ethora-color-primary-soft, #e7edf9);
|
|
62
|
-
color: var(--ethora-color-primary, #0052cd);
|
|
62
|
+
color: var(--ethora-color-primary-text, var(--ethora-color-primary, #0052cd));
|
|
63
63
|
font-size: var(--ethora-font-size-xs, 12px);
|
|
64
64
|
font-weight: var(--ethora-font-weight-semibold, 600);
|
|
65
65
|
|