@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.
Files changed (48) hide show
  1. package/README.md +1 -0
  2. package/dist/{ChatProfileModal-DZVi2WRb.js → ChatProfileModal-DfXEnzJf.js} +6 -3
  3. package/dist/{FilePreviewModal-BelSYN5e.js → FilePreviewModal-Cs1tij9R.js} +35 -35
  4. package/dist/{ManageDataModal-Bs_0SHBL.js → ManageDataModal-BBOui09n.js} +3 -3
  5. package/dist/{MarkdownBody-BYUK4GVM.js → MarkdownBody-DttRXyAK.js} +3 -3
  6. package/dist/{SideDrawer-C_nWZRwH.js → SideDrawer-DLqEOqAi.js} +35 -35
  7. package/dist/UserProfileModal-DEtLQxkJ.js +594 -0
  8. package/dist/{UserSettingsModal-J7uJfki1.js → UserSettingsModal-Ddht5fc0.js} +9 -9
  9. package/dist/{VideoCallOverlay-BXHNEhdX.js → VideoCallOverlay-BJMarThf.js} +2673 -2662
  10. package/dist/VisibilityModal-CckTnGKB.js +138 -0
  11. package/dist/assets/icons.d.ts +8 -0
  12. package/dist/components/MainComponents/AttachmentList.d.ts +5 -0
  13. package/dist/components/MainComponents/SealedAttachmentList.d.ts +14 -0
  14. package/dist/components/RoomComponents/styled/LastMessageSealed.d.ts +14 -0
  15. package/dist/components/styled/AudioMessage.d.ts +38 -0
  16. package/dist/components/styled/MessageImage.d.ts +10 -0
  17. package/dist/e2ee/crypto.d.ts +32 -0
  18. package/dist/e2ee/fileEnvelope.d.ts +48 -0
  19. package/dist/e2ee/index.d.ts +53 -0
  20. package/dist/e2ee/omemo.d.ts +130 -0
  21. package/dist/e2ee/protobuf.d.ts +25 -0
  22. package/dist/e2ee/ratchet.d.ts +54 -0
  23. package/dist/e2ee/stanza.d.ts +24 -0
  24. package/dist/e2ee/store.d.ts +12 -0
  25. package/dist/{handleCopyClick-Bj2V_brV.js → handleCopyClick-CWw4UAlT.js} +2 -2
  26. package/dist/helpers/findMessagesForFile.d.ts +25 -0
  27. package/dist/helpers/sealedAttachments.d.ts +25 -0
  28. package/dist/helpers/xmppIdShape.d.ts +9 -0
  29. package/dist/helpers/xmppUsername.d.ts +1 -1
  30. package/dist/hooks/useChatWrapperInit.d.ts +5 -1
  31. package/dist/{main-DmtEeYji.js → main-BHtKhpBZ.js} +21004 -16820
  32. package/dist/main.d.ts +2 -0
  33. package/dist/main.js +23 -20
  34. package/dist/networking/api-requests/auth.api.d.ts +10 -1
  35. package/dist/networking/api-requests/rooms.api.d.ts +1 -1
  36. package/dist/networking/authRefresh.d.ts +6 -0
  37. package/dist/networking/xmpp/sendMediaMessage.xmpp.d.ts +12 -1
  38. package/dist/networking/xmpp/sendTextMessage.xmpp.d.ts +1 -1
  39. package/dist/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.d.ts +1 -1
  40. package/dist/networking/xmppClient.d.ts +39 -0
  41. package/dist/roomStore/storage.d.ts +2 -1
  42. package/dist/styles/tokens.d.ts +14 -4
  43. package/dist/types/models/config.model.d.ts +33 -0
  44. package/dist/types/models/message.model.d.ts +27 -0
  45. package/dist/types/models/room.model.d.ts +10 -0
  46. package/package.json +4 -1
  47. package/dist/UserProfileModal-CD2IB5SD.js +0 -574
  48. 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
+ };
@@ -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 { v as e, x as t } from "./main-DmtEeYji.js";
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