@agora-sdk/secure-chat-core 0.6.5 → 0.8.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/dist/cjs/content/builders.d.ts +40 -0
- package/dist/cjs/content/builders.js +85 -0
- package/dist/cjs/content/builders.js.map +1 -0
- package/dist/cjs/content/cbor.d.ts +48 -0
- package/dist/cjs/content/cbor.js +298 -0
- package/dist/cjs/content/cbor.js.map +1 -0
- package/dist/cjs/content/frame.d.ts +24 -0
- package/dist/cjs/content/frame.js +39 -0
- package/dist/cjs/content/frame.js.map +1 -0
- package/dist/cjs/content/mimi-content.d.ts +194 -0
- package/dist/cjs/content/mimi-content.js +289 -0
- package/dist/cjs/content/mimi-content.js.map +1 -0
- package/dist/cjs/context/secure-chat-context.d.ts +19 -3
- package/dist/cjs/context/secure-chat-context.js +53 -13
- package/dist/cjs/context/secure-chat-context.js.map +1 -1
- package/dist/cjs/hooks/message-fold.d.ts +91 -0
- package/dist/cjs/hooks/message-fold.js +218 -0
- package/dist/cjs/hooks/message-fold.js.map +1 -0
- package/dist/cjs/hooks/useSecureConversations.js +11 -0
- package/dist/cjs/hooks/useSecureConversations.js.map +1 -1
- package/dist/cjs/hooks/useSecureDevice.js +15 -1
- package/dist/cjs/hooks/useSecureDevice.js.map +1 -1
- package/dist/cjs/hooks/useSecureHandshakes.js +36 -2
- package/dist/cjs/hooks/useSecureHandshakes.js.map +1 -1
- package/dist/cjs/hooks/useSecureMessages.d.ts +30 -18
- package/dist/cjs/hooks/useSecureMessages.js +197 -130
- package/dist/cjs/hooks/useSecureMessages.js.map +1 -1
- package/dist/cjs/index.d.ts +8 -0
- package/dist/cjs/index.js +30 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/persistence/repository.d.ts +24 -0
- package/dist/cjs/persistence/repository.js +35 -0
- package/dist/cjs/persistence/repository.js.map +1 -1
- package/dist/cjs/persistence/store.js +11 -0
- package/dist/cjs/persistence/store.js.map +1 -1
- package/dist/cjs/transport/rest.d.ts +2 -2
- package/dist/cjs/transport/rest.js +2 -2
- package/dist/cjs/transport/rest.js.map +1 -1
- package/dist/cjs/transport/socket.d.ts +1 -1
- package/dist/cjs/transport/socket.js +17 -1
- package/dist/cjs/transport/socket.js.map +1 -1
- package/dist/cjs/version.d.ts +13 -0
- package/dist/cjs/version.js +19 -0
- package/dist/cjs/version.js.map +1 -0
- package/dist/esm/content/builders.d.ts +40 -0
- package/dist/esm/content/builders.js +77 -0
- package/dist/esm/content/builders.js.map +1 -0
- package/dist/esm/content/cbor.d.ts +48 -0
- package/dist/esm/content/cbor.js +292 -0
- package/dist/esm/content/cbor.js.map +1 -0
- package/dist/esm/content/frame.d.ts +24 -0
- package/dist/esm/content/frame.js +34 -0
- package/dist/esm/content/frame.js.map +1 -0
- package/dist/esm/content/mimi-content.d.ts +194 -0
- package/dist/esm/content/mimi-content.js +283 -0
- package/dist/esm/content/mimi-content.js.map +1 -0
- package/dist/esm/context/secure-chat-context.d.ts +19 -3
- package/dist/esm/context/secure-chat-context.js +53 -13
- package/dist/esm/context/secure-chat-context.js.map +1 -1
- package/dist/esm/hooks/message-fold.d.ts +91 -0
- package/dist/esm/hooks/message-fold.js +214 -0
- package/dist/esm/hooks/message-fold.js.map +1 -0
- package/dist/esm/hooks/useSecureConversations.js +11 -0
- package/dist/esm/hooks/useSecureConversations.js.map +1 -1
- package/dist/esm/hooks/useSecureDevice.js +15 -1
- package/dist/esm/hooks/useSecureDevice.js.map +1 -1
- package/dist/esm/hooks/useSecureHandshakes.js +36 -2
- package/dist/esm/hooks/useSecureHandshakes.js.map +1 -1
- package/dist/esm/hooks/useSecureMessages.d.ts +30 -18
- package/dist/esm/hooks/useSecureMessages.js +199 -132
- package/dist/esm/hooks/useSecureMessages.js.map +1 -1
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/persistence/repository.d.ts +24 -0
- package/dist/esm/persistence/repository.js +35 -0
- package/dist/esm/persistence/repository.js.map +1 -1
- package/dist/esm/persistence/store.js +11 -0
- package/dist/esm/persistence/store.js.map +1 -1
- package/dist/esm/transport/rest.d.ts +2 -2
- package/dist/esm/transport/rest.js +2 -2
- package/dist/esm/transport/rest.js.map +1 -1
- package/dist/esm/transport/socket.d.ts +1 -1
- package/dist/esm/transport/socket.js +17 -1
- package/dist/esm/transport/socket.js.map +1 -1
- package/dist/esm/version.d.ts +13 -0
- package/dist/esm/version.js +16 -0
- package/dist/esm/version.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// MimiContent — secure-chat's message content format. IMPLEMENTS draft-ietf-mimi-content-08 (2 Mar
|
|
2
|
+
// 2026), the message-content CDDL. The CBOR WIRE STRUCTURE below is draft-08-faithful with NO deviation
|
|
3
|
+
// (the interop-readiness payoff); the ONE deliberate deviation is MessageId DERIVATION (we lack
|
|
4
|
+
// federation identities) — documented in full at the bottom of this header.
|
|
5
|
+
//
|
|
6
|
+
// Where it sits: pure structure ABOVE the SecureChatCrypto seam and INSIDE the padding frame. Content
|
|
7
|
+
// is a core concern, never crypto's. We encode/decode the FULL draft structure (so Tier-3 fields
|
|
8
|
+
// round-trip today and surface later without rework); the hook surfaces Tier 2 (text / reply /
|
|
9
|
+
// reaction / edit / delete). Decoded bytes are UNTRUSTED peer input: decodeMimiContent validates the
|
|
10
|
+
// schema AFTER canonical CBOR-decoding and fails closed (CLAUDE.md §1).
|
|
11
|
+
//
|
|
12
|
+
// Wire CDDL (draft-ietf-mimi-content-08, the message-content section) — exactly what we encode/decode:
|
|
13
|
+
//
|
|
14
|
+
// mimiContent = [salt:bstr.size 16, replaces:null/MessageId, topicId:bstr,
|
|
15
|
+
// expires:null/Expiration, inReplyTo:null/MessageId,
|
|
16
|
+
// mimiExtensions:extensions, nestedPart:NestedPart] ; 7 elements
|
|
17
|
+
// MessageId = bstr.size 32
|
|
18
|
+
// Expiration = [relative:bool, time:uint.size 4]
|
|
19
|
+
// extensions = { ? &(senderUri:1)^ => tstr, ? &(roomUri:2)^ => tstr, * otherKnown, * unknown }
|
|
20
|
+
// name = int / tstr.size (1..255) value = any.size (0..4095)
|
|
21
|
+
// NestedPart = [disposition, language:tstr, (NullPart // SinglePart // ExternalPart // MultiPart)]
|
|
22
|
+
// NullPart = (cardinality:nullpart)
|
|
23
|
+
// SinglePart = (cardinality:single, contentType:tstr, content:bstr)
|
|
24
|
+
// ExternalPart= (cardinality:external, contentType:tstr, url:tstr, expires:uint.size 4,
|
|
25
|
+
// size:uint.size 8, encAlg:uint.size 2, key:bstr, nonce:bstr, aad:bstr,
|
|
26
|
+
// hashAlg:uint.size 1, contentHash:bstr, description:tstr, filename:tstr)
|
|
27
|
+
// MultiPart = (cardinality:multi, partSemantics, parts:[2* NestedPart]) ; ≥2 parts
|
|
28
|
+
// baseDispos: unspecified=0 render=1 reaction=2 profile=3 inline=4 icon=5 attachment=6 session=7 preview=8
|
|
29
|
+
// cardinality: nullpart=0 single=1 external=2 multi=3 partSemantics: chooseOne=0 singleUnit=1 processAll=2
|
|
30
|
+
// SHA-256 hash-algorithm identifier = 0x01
|
|
31
|
+
//
|
|
32
|
+
// Because NestedPart hoists `disposition` + `language` OUT of the variant, a single part encodes to the
|
|
33
|
+
// CBOR array [disposition, language, cardinality, …variantFields]. Our TS `Part` types keep
|
|
34
|
+
// `disposition`/`language` ON each variant (so the reducer can branch on `part.disposition ===
|
|
35
|
+
// Disposition.Reaction` and builders can set it); the codec maps them to/from the wrapper positions.
|
|
36
|
+
//
|
|
37
|
+
// ── THE ONE DELIBERATE DEVIATION: MessageId derivation ───────────────────────────────────────────
|
|
38
|
+
// draft-08 derives a MessageId from FEDERATION IDENTITIES:
|
|
39
|
+
// messageId = 0x01 || SHA256(senderUriLen || senderUri || roomUriLen || roomUri || message || salt)[0..30]
|
|
40
|
+
// This SDK adopts the MIMI content FORMAT, not MIMI federation (design spec Goal A) — we have no
|
|
41
|
+
// senderUri / roomUri. So `contentHash(content)` returns a 32-byte MessageId-SHAPED value:
|
|
42
|
+
// 0x01 (SHA-256 hashAlg) || sha256(encodeMimiContent(content))[0..30] (1 + 31 = 32 bytes)
|
|
43
|
+
// — structurally a real MIMI MessageId; only the hash INPUT is simplified (our canonical CBOR, which
|
|
44
|
+
// already includes the per-message `salt`, with no URI prefixes). `replaces`/`inReplyTo` carry THIS
|
|
45
|
+
// value. This is the single upgrade-when-federation-lands deviation; the WIRE CBOR has none.
|
|
46
|
+
//
|
|
47
|
+
// Design-doc reconcile (FOLLOWING THE DRAFT; for the doc owner): the doc's top-level `lastSeen` list,
|
|
48
|
+
// the per-Part `partIndex`, and `width`/`height`/`duration` on ExternalPart are NOT in draft-08 and are
|
|
49
|
+
// DROPPED; the doc modeled `inReplyTo` as {hash, hashAlgorithm} but the draft uses a bare 32-byte
|
|
50
|
+
// MessageId, so `inReplyTo` is now `Uint8Array | null` and the `MessageDerivedValue` type is removed.
|
|
51
|
+
import { encode, decode } from "./cbor.js";
|
|
52
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
53
|
+
/**
|
|
54
|
+
* Part cardinality tag. Values per draft-ietf-mimi-content-08
|
|
55
|
+
* (`nullpart=0, single=1, external=2, multi=3`).
|
|
56
|
+
*/
|
|
57
|
+
export const Cardinality = { Null: 0, Single: 1, External: 2, Multi: 3 };
|
|
58
|
+
/**
|
|
59
|
+
* Part disposition (draft-ietf-mimi-content-08 `baseDispos`). `Reaction` distinguishes a reaction
|
|
60
|
+
* from a reply; `Attachment`/`Inline`/etc. drive client rendering.
|
|
61
|
+
*/
|
|
62
|
+
export const Disposition = {
|
|
63
|
+
Unspecified: 0, Render: 1, Reaction: 2, Profile: 3, Inline: 4, Icon: 5,
|
|
64
|
+
Attachment: 6, Session: 7, Preview: 8,
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* {@link MultiPart} `partSemantics` — how a client treats the child parts (draft-ietf-mimi-content-08:
|
|
68
|
+
* `chooseOne=0, singleUnit=1, processAll=2`).
|
|
69
|
+
*/
|
|
70
|
+
export const PartSemantics = { ChooseOne: 0, SingleUnit: 1, ProcessAll: 2 };
|
|
71
|
+
/**
|
|
72
|
+
* Named-Information hash algorithm. SHA-256 is the only value we emit (identifier `0x01`, per
|
|
73
|
+
* draft-ietf-mimi-content-08).
|
|
74
|
+
*/
|
|
75
|
+
export const HashAlg = { Sha256: 1 };
|
|
76
|
+
/**
|
|
77
|
+
* Schema bounds enforced on decode (DoS / abuse guard): max sibling parts in a {@link MultiPart}, and
|
|
78
|
+
* max part-tree depth. (draft-08 has no `lastSeen`, so there is no list bound here.)
|
|
79
|
+
*/
|
|
80
|
+
export const MIMI_LIMITS = { maxParts: 64, maxNesting: 8 };
|
|
81
|
+
// ── encode ────────────────────────────────────────────────────────────────────
|
|
82
|
+
function encodePart(p) {
|
|
83
|
+
// NestedPart = [disposition, language, …variant-starting-with-cardinality].
|
|
84
|
+
switch (p.cardinality) {
|
|
85
|
+
case Cardinality.Null:
|
|
86
|
+
return [p.disposition, p.language, Cardinality.Null];
|
|
87
|
+
case Cardinality.Single:
|
|
88
|
+
return [p.disposition, p.language, Cardinality.Single, p.contentType, p.content];
|
|
89
|
+
case Cardinality.External:
|
|
90
|
+
return [
|
|
91
|
+
p.disposition, p.language, Cardinality.External, p.contentType, p.url, p.expires, p.size,
|
|
92
|
+
p.encAlgorithm, p.key, p.nonce, p.aad, p.hashAlgorithm, p.contentHash, p.description, p.filename,
|
|
93
|
+
];
|
|
94
|
+
case Cardinality.Multi:
|
|
95
|
+
return [p.disposition, p.language, Cardinality.Multi, p.partSemantics, p.parts.map(encodePart)];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function toCbor(c) {
|
|
99
|
+
return [
|
|
100
|
+
c.salt,
|
|
101
|
+
c.replaces, // bstr(32) | null
|
|
102
|
+
c.topicId,
|
|
103
|
+
c.expires ? [c.expires.relative, c.expires.time] : null, // Expiration | null
|
|
104
|
+
c.inReplyTo, // bstr(32) | null
|
|
105
|
+
c.extensions,
|
|
106
|
+
encodePart(c.nestedPart),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Encode a {@link MimiContent} to canonical CBOR bytes per the draft-08 wire layout.
|
|
111
|
+
* @param c - The content to encode.
|
|
112
|
+
* @returns Canonical CBOR bytes (stable for equal content — the basis for {@link contentHash}).
|
|
113
|
+
* @throws {Error} If a field carries an out-of-subset CBOR value (e.g. a non-integer number).
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* const bytes = encodeMimiContent(myContent);
|
|
117
|
+
* const same = decodeMimiContent(bytes); // round-trips
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function encodeMimiContent(c) {
|
|
121
|
+
return encode(toCbor(c));
|
|
122
|
+
}
|
|
123
|
+
// ── decode + validate ───────────────────────────────────────────────────────
|
|
124
|
+
function asArray(v, ctx) {
|
|
125
|
+
if (!Array.isArray(v))
|
|
126
|
+
throw new Error(`MimiContent: expected array (${ctx})`);
|
|
127
|
+
return v;
|
|
128
|
+
}
|
|
129
|
+
function asBytes(v, ctx) {
|
|
130
|
+
if (!(v instanceof Uint8Array))
|
|
131
|
+
throw new Error(`MimiContent: expected bytes (${ctx})`);
|
|
132
|
+
return v;
|
|
133
|
+
}
|
|
134
|
+
function asString(v, ctx) {
|
|
135
|
+
if (typeof v !== "string")
|
|
136
|
+
throw new Error(`MimiContent: expected string (${ctx})`);
|
|
137
|
+
return v;
|
|
138
|
+
}
|
|
139
|
+
function asInt(v, ctx) {
|
|
140
|
+
if (typeof v !== "number" || !Number.isInteger(v))
|
|
141
|
+
throw new Error(`MimiContent: expected int (${ctx})`);
|
|
142
|
+
return v;
|
|
143
|
+
}
|
|
144
|
+
function asBool(v, ctx) {
|
|
145
|
+
if (typeof v !== "boolean")
|
|
146
|
+
throw new Error(`MimiContent: expected bool (${ctx})`);
|
|
147
|
+
return v;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Exact wire-array length per cardinality (`[disposition, language, cardinality, …variant]`). Decode
|
|
151
|
+
* asserts this exactly so a part carrying TRAILING junk is rejected, not silently dropped (CLAUDE.md
|
|
152
|
+
* §1: fail closed — never skip the check).
|
|
153
|
+
*/
|
|
154
|
+
const PART_ARITY = {
|
|
155
|
+
[Cardinality.Null]: 3, // disposition, language, cardinality
|
|
156
|
+
[Cardinality.Single]: 5, // + contentType, content
|
|
157
|
+
// disposition, language, cardinality + 12 ExternalPart fields (contentType, url, expires, size,
|
|
158
|
+
// encAlg, key, nonce, aad, hashAlg, contentHash, description, filename) = 15. (decode reads a[14].)
|
|
159
|
+
[Cardinality.External]: 15,
|
|
160
|
+
[Cardinality.Multi]: 5, // + partSemantics, parts
|
|
161
|
+
};
|
|
162
|
+
function decodePart(v, depth) {
|
|
163
|
+
if (depth > MIMI_LIMITS.maxNesting)
|
|
164
|
+
throw new Error("MimiContent: part nesting exceeds bounds");
|
|
165
|
+
const a = asArray(v, "part");
|
|
166
|
+
// NestedPart wrapper: [disposition, language, cardinality, …variant].
|
|
167
|
+
const disposition = asInt(a[0], "part.disposition");
|
|
168
|
+
const language = asString(a[1], "part.language");
|
|
169
|
+
const card = asInt(a[2], "part.cardinality");
|
|
170
|
+
// Fail closed on an unknown cardinality OR a wrong-length part array (trailing/missing elements).
|
|
171
|
+
const expectedArity = PART_ARITY[card];
|
|
172
|
+
if (expectedArity === undefined)
|
|
173
|
+
throw new Error(`MimiContent: unknown part cardinality ${card}`);
|
|
174
|
+
if (a.length !== expectedArity)
|
|
175
|
+
throw new Error(`MimiContent: wrong arity for cardinality ${card}`);
|
|
176
|
+
switch (card) {
|
|
177
|
+
case Cardinality.Null:
|
|
178
|
+
return { cardinality: Cardinality.Null, disposition, language };
|
|
179
|
+
case Cardinality.Single:
|
|
180
|
+
return {
|
|
181
|
+
cardinality: Cardinality.Single,
|
|
182
|
+
disposition,
|
|
183
|
+
language,
|
|
184
|
+
contentType: asString(a[3], "contentType"),
|
|
185
|
+
content: asBytes(a[4], "content"),
|
|
186
|
+
};
|
|
187
|
+
case Cardinality.External:
|
|
188
|
+
return {
|
|
189
|
+
cardinality: Cardinality.External,
|
|
190
|
+
disposition,
|
|
191
|
+
language,
|
|
192
|
+
contentType: asString(a[3], "contentType"),
|
|
193
|
+
url: asString(a[4], "url"),
|
|
194
|
+
expires: asInt(a[5], "expires"),
|
|
195
|
+
size: asInt(a[6], "size"),
|
|
196
|
+
encAlgorithm: asInt(a[7], "encAlgorithm"),
|
|
197
|
+
key: asBytes(a[8], "key"),
|
|
198
|
+
nonce: asBytes(a[9], "nonce"),
|
|
199
|
+
aad: asBytes(a[10], "aad"),
|
|
200
|
+
hashAlgorithm: asInt(a[11], "hashAlgorithm"),
|
|
201
|
+
contentHash: asBytes(a[12], "contentHash"),
|
|
202
|
+
description: asString(a[13], "description"),
|
|
203
|
+
filename: asString(a[14], "filename"),
|
|
204
|
+
};
|
|
205
|
+
case Cardinality.Multi: {
|
|
206
|
+
const parts = asArray(a[4], "multi.parts");
|
|
207
|
+
// The draft requires 2 or more child parts; reject a degenerate multipart fail-closed.
|
|
208
|
+
if (parts.length < 2)
|
|
209
|
+
throw new Error("MimiContent: multipart requires 2+ parts");
|
|
210
|
+
if (parts.length > MIMI_LIMITS.maxParts)
|
|
211
|
+
throw new Error("MimiContent: too many parts (bounds)");
|
|
212
|
+
return {
|
|
213
|
+
cardinality: Cardinality.Multi,
|
|
214
|
+
disposition,
|
|
215
|
+
language,
|
|
216
|
+
partSemantics: asInt(a[3], "partSemantics"),
|
|
217
|
+
parts: parts.map((p) => decodePart(p, depth + 1)),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
throw new Error(`MimiContent: unknown part cardinality ${card}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Strict-decode + schema-validate canonical CBOR into a {@link MimiContent}. Fails closed on any
|
|
226
|
+
* structural or bounds violation (untrusted peer input — CLAUDE.md §1).
|
|
227
|
+
* @param bytes - The canonical CBOR content bytes (after unframing).
|
|
228
|
+
* @returns The validated {@link MimiContent}.
|
|
229
|
+
* @throws {Error} On a malformed structure, wrong field type, an unknown {@link Cardinality}, or a
|
|
230
|
+
* {@link MIMI_LIMITS} bound exceeded. The error message never includes message plaintext.
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* const content = decodeMimiContent(peerBytes); // throws on any tampering
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function decodeMimiContent(bytes) {
|
|
237
|
+
const a = asArray(decode(bytes, { maxItems: MIMI_LIMITS.maxParts * 4 }), "MimiContent");
|
|
238
|
+
if (a.length !== 7)
|
|
239
|
+
throw new Error("MimiContent: wrong top-level arity");
|
|
240
|
+
const replaces = a[1] === null ? null : asBytes(a[1], "replaces");
|
|
241
|
+
let expires = null;
|
|
242
|
+
if (a[3] !== null) {
|
|
243
|
+
const e = asArray(a[3], "expires");
|
|
244
|
+
expires = { relative: asBool(e[0], "expires.relative"), time: asInt(e[1], "expires.time") };
|
|
245
|
+
}
|
|
246
|
+
const inReplyTo = a[4] === null ? null : asBytes(a[4], "inReplyTo");
|
|
247
|
+
const ext = a[5];
|
|
248
|
+
if (!(ext instanceof Map))
|
|
249
|
+
throw new Error("MimiContent: extensions must be a map");
|
|
250
|
+
return {
|
|
251
|
+
salt: asBytes(a[0], "salt"),
|
|
252
|
+
replaces,
|
|
253
|
+
topicId: asBytes(a[2], "topicId"),
|
|
254
|
+
expires,
|
|
255
|
+
inReplyTo,
|
|
256
|
+
extensions: ext,
|
|
257
|
+
nestedPart: decodePart(a[6], 0),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The MIMI content-hash: a 32-byte, MessageId-SHAPED reference value used to point at a message
|
|
262
|
+
* (reply / edit / delete / un-react). Per the file-header deviation note, we lack federation
|
|
263
|
+
* identities, so instead of draft-08's URI-prefixed input we compute
|
|
264
|
+
* `0x01 || sha256(encodeMimiContent(c))[0..30]` — the leading `0x01` is the SHA-256 hash-algorithm
|
|
265
|
+
* identifier, then the first 31 bytes of the SHA-256 over our canonical CBOR (which already includes
|
|
266
|
+
* the per-message `salt`). Structurally a real MIMI MessageId; stable across runtimes; salt-sensitive.
|
|
267
|
+
* The blind server never sees this. `replaces` / `inReplyTo` are populated with this value.
|
|
268
|
+
* @param c - The content to hash.
|
|
269
|
+
* @returns A 32-byte MessageId (`[0]` is `0x01`; `[1..31]` are `sha256(canonical CBOR)[0..30]`).
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const id = contentHash(myContent); // 32 bytes, id[0] === 0x01, salt-sensitive
|
|
273
|
+
* reply.inReplyTo = id;
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
export function contentHash(c) {
|
|
277
|
+
const digest = sha256(encodeMimiContent(c));
|
|
278
|
+
const messageId = new Uint8Array(32);
|
|
279
|
+
messageId[0] = HashAlg.Sha256; // 0x01 — the MessageId hashAlg prefix byte
|
|
280
|
+
messageId.set(digest.subarray(0, 31), 1); // first 31 SHA-256 bytes fill [1..31]
|
|
281
|
+
return messageId;
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=mimi-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mimi-content.js","sourceRoot":"","sources":["../../../src/content/mimi-content.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,15 @@ export interface SecureChatContextValue {
|
|
|
22
22
|
resolveGroup: (conversationId: string) => Promise<GroupHandle | null>;
|
|
23
23
|
/** Cache + persist a conversation's group handle (after createGroup / processWelcome). */
|
|
24
24
|
rememberGroup: (conversationId: string, handle: GroupHandle) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Persist a conversation's CURRENT group state after an intra-epoch ratchet advance — i.e. an
|
|
27
|
+
* application message we just sent or received. Unlike {@link rememberGroup} it does NOT bump the
|
|
28
|
+
* version or notify listeners: an application message moves the (single-use, forward-secret) MLS
|
|
29
|
+
* ratchet but NOT the epoch, so consumers must not re-resolve the handle. Skipping this persist is
|
|
30
|
+
* the resend-replay bug — on reload the send ratchet rewinds to a consumed generation and the peer
|
|
31
|
+
* rejects the next message as a replay.
|
|
32
|
+
*/
|
|
33
|
+
persistGroupState: (conversationId: string, handle: GroupHandle) => Promise<void>;
|
|
25
34
|
/**
|
|
26
35
|
* Current change-version for a conversation's group handle. Bumps every time the handle advances
|
|
27
36
|
* (a join or a processed Commit), so consumers can detect "the group moved" without diffing handles.
|
|
@@ -52,9 +61,16 @@ export interface SecureChatProviderProps {
|
|
|
52
61
|
accessToken?: string;
|
|
53
62
|
/** Override token resolution (takes precedence over `accessToken`). */
|
|
54
63
|
getAccessToken?: () => string | undefined;
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
/**
|
|
65
|
+
* API base URL including the version prefix (e.g. `https://api.example.com/v7`). Required — secure
|
|
66
|
+
* chat is a standalone transport and does not resolve a URL from any ambient SDK runtime. Read
|
|
67
|
+
* lazily per request, so re-passing a changed value takes effect on the next call.
|
|
68
|
+
*/
|
|
69
|
+
baseUrl: string;
|
|
70
|
+
/**
|
|
71
|
+
* Socket.io origin for the `/secure` realtime namespace. Defaults to {@link baseUrl} (the client
|
|
72
|
+
* strips any path to the origin), so pass it only when the socket lives on a different host.
|
|
73
|
+
*/
|
|
58
74
|
socketUrl?: string;
|
|
59
75
|
/**
|
|
60
76
|
* Outbound message size-bucket padding policy (metadata hardening). `"ladder"` (default) pads each
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// SecureChatProvider — wires transport + crypto + persistence for the secure-chat hooks.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// @agora-sdk/core
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// self-
|
|
4
|
+
// Standalone transport: the caller supplies the API base URL (and optional socket origin) directly —
|
|
5
|
+
// this package has NO dependency on @agora-sdk/core. (It used to fall back to core's getApiBaseUrl /
|
|
6
|
+
// getSocketUrl runtime singletons; that coupling existed only to auto-inherit a Replyke app's config,
|
|
7
|
+
// and core's actual surface here was just two URL accessors. Requiring `baseUrl` makes secure chat a
|
|
8
|
+
// self-contained E2EE transport usable in any app.) Crypto AND the persistence store are injected too,
|
|
9
|
+
// keeping this layer platform- and library-agnostic. The provider builds a typed SecureChatRepository
|
|
10
|
+
// over the store plus a cached resolveGroup/rememberGroup so the hooks become self-sufficient (no need
|
|
11
|
+
// to thread a GroupHandle in by hand).
|
|
9
12
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
10
|
-
import { getApiBaseUrl, getSocketUrl } from "@agora-sdk/core";
|
|
11
13
|
import { SecureChatRestClient } from "../transport/rest.js";
|
|
12
14
|
import { SecureChatSocketClient } from "../transport/socket.js";
|
|
13
15
|
import { MemoryStore } from "../persistence/memory-store.js";
|
|
14
16
|
import { SecureChatRepository } from "../persistence/repository.js";
|
|
17
|
+
import { createDebugLogger } from "../util/debug.js";
|
|
18
|
+
const log = createDebugLogger("provider");
|
|
15
19
|
const SecureChatContext = createContext(null);
|
|
16
20
|
/**
|
|
17
21
|
* Provides secure-chat transport, crypto, and persistence to the `useSecure*` hooks. Render inside a
|
|
@@ -34,13 +38,14 @@ export function SecureChatProvider({ crypto, projectId, store, accessToken, getA
|
|
|
34
38
|
const rest = useMemo(() => new SecureChatRestClient({
|
|
35
39
|
projectId,
|
|
36
40
|
getAccessToken: resolveToken,
|
|
37
|
-
getBaseUrl: () => baseUrl
|
|
41
|
+
getBaseUrl: () => baseUrl,
|
|
38
42
|
}), [projectId, resolveToken, baseUrl]);
|
|
39
43
|
const socket = useMemo(() => new SecureChatSocketClient({
|
|
40
44
|
projectId,
|
|
41
45
|
getAccessToken: resolveToken,
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// Socket origin defaults to the REST base URL (the client strips the path to an origin).
|
|
47
|
+
getSocketUrl: () => socketUrl ?? baseUrl,
|
|
48
|
+
}), [projectId, resolveToken, socketUrl, baseUrl]);
|
|
44
49
|
const resolvedStore = useMemo(() => store ?? new MemoryStore(), [store]);
|
|
45
50
|
const repo = useMemo(() => new SecureChatRepository(resolvedStore), [resolvedStore]);
|
|
46
51
|
// In-memory GroupHandle cache, keyed by conversationId. Survives re-renders via the ref.
|
|
@@ -58,11 +63,33 @@ export function SecureChatProvider({ crypto, projectId, store, accessToken, getA
|
|
|
58
63
|
if (cached)
|
|
59
64
|
return cached;
|
|
60
65
|
const bytes = await repo.loadGroupState(conversationId);
|
|
61
|
-
if (!bytes)
|
|
66
|
+
if (!bytes) {
|
|
67
|
+
// No persisted group for this conversation: the recipient never joined (no Welcome processed),
|
|
68
|
+
// or local state was wiped. Renders as "waiting for key update" until a Welcome arrives.
|
|
69
|
+
log.debug("resolveGroup: no persisted group state", { conversationId });
|
|
62
70
|
return null;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const handle = await crypto.importGroupState(bytes);
|
|
74
|
+
groupCache.current.set(conversationId, handle);
|
|
75
|
+
log.debug("resolveGroup: imported group from store", {
|
|
76
|
+
conversationId,
|
|
77
|
+
epoch: handle.epoch.toString(),
|
|
78
|
+
bytes: bytes.length,
|
|
79
|
+
});
|
|
80
|
+
return handle;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// A persisted group that can't be re-imported is a black hole — it silently degrades to "no
|
|
84
|
+
// group" (a permanent "waiting for key update"). Surface it loudly so a reload-persistence or
|
|
85
|
+
// crypto-version-skew problem is visible instead of looking like an un-joined conversation.
|
|
86
|
+
log.debug("resolveGroup: importGroupState FAILED — group present but unreadable", {
|
|
87
|
+
conversationId,
|
|
88
|
+
bytes: bytes.length,
|
|
89
|
+
error: err instanceof Error ? err.message : String(err),
|
|
90
|
+
});
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
66
93
|
}, [repo, crypto]);
|
|
67
94
|
const rememberGroup = useCallback(async (conversationId, handle) => {
|
|
68
95
|
groupCache.current.set(conversationId, handle);
|
|
@@ -72,6 +99,17 @@ export function SecureChatProvider({ crypto, projectId, store, accessToken, getA
|
|
|
72
99
|
groupVersion.current.set(conversationId, (groupVersion.current.get(conversationId) ?? 0) + 1);
|
|
73
100
|
groupListeners.current.forEach((l) => l());
|
|
74
101
|
}, [repo, crypto]);
|
|
102
|
+
const persistGroupState = useCallback(async (conversationId, handle) => {
|
|
103
|
+
// Re-export the now-advanced ratchet for THIS group and overwrite the persisted blob. The
|
|
104
|
+
// ratchet state lives in the crypto's internal per-group map (keyed by mlsGroupId), so exporting
|
|
105
|
+
// the same handle after a send/receive captures the advanced generation. Deliberately NO version
|
|
106
|
+
// bump and NO listener notify (cf. rememberGroup): an application message is intra-epoch, so a
|
|
107
|
+
// re-resolve would be wasted work and could churn buffered-row retries. The cache holds the same
|
|
108
|
+
// handle object the hook already uses, so re-setting it is idempotent.
|
|
109
|
+
groupCache.current.set(conversationId, handle);
|
|
110
|
+
const bytes = await crypto.exportGroupState(handle);
|
|
111
|
+
await repo.saveGroupState(conversationId, bytes);
|
|
112
|
+
}, [repo, crypto]);
|
|
75
113
|
const getGroupVersion = useCallback((conversationId) => groupVersion.current.get(conversationId) ?? 0, []);
|
|
76
114
|
const subscribeGroupChange = useCallback((listener) => {
|
|
77
115
|
groupListeners.current.add(listener);
|
|
@@ -89,6 +127,7 @@ export function SecureChatProvider({ crypto, projectId, store, accessToken, getA
|
|
|
89
127
|
repo,
|
|
90
128
|
resolveGroup,
|
|
91
129
|
rememberGroup,
|
|
130
|
+
persistGroupState,
|
|
92
131
|
getGroupVersion,
|
|
93
132
|
subscribeGroupChange,
|
|
94
133
|
padding,
|
|
@@ -100,6 +139,7 @@ export function SecureChatProvider({ crypto, projectId, store, accessToken, getA
|
|
|
100
139
|
repo,
|
|
101
140
|
resolveGroup,
|
|
102
141
|
rememberGroup,
|
|
142
|
+
persistGroupState,
|
|
103
143
|
getGroupVersion,
|
|
104
144
|
subscribeGroupChange,
|
|
105
145
|
padding,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-chat-context.js","sourceRoot":"","sources":["../../../src/context/secure-chat-context.tsx"],"names":[],"mappings":";AAAA,yFAAyF;AACzF,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"secure-chat-context.js","sourceRoot":"","sources":["../../../src/context/secure-chat-context.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type MimiContent } from "../content/mimi-content.js";
|
|
2
|
+
/** A decoded, content-hashed message handed to the fold (the hook computes these post-decrypt). */
|
|
3
|
+
export interface DecodedContentMessage {
|
|
4
|
+
/** Server message id (storage/order/dedup key). */
|
|
5
|
+
messageId: string;
|
|
6
|
+
/** Server createdAt (ISO) — used to stamp `editedAt` and for the hook's ordering. */
|
|
7
|
+
createdAt: string;
|
|
8
|
+
/** Authenticated MLS sender device id. */
|
|
9
|
+
senderDeviceId: string;
|
|
10
|
+
/** SHA-256 over the canonical MimiContent bytes (the reference key). */
|
|
11
|
+
contentHash: Uint8Array;
|
|
12
|
+
/** The decoded content. */
|
|
13
|
+
mimi: MimiContent;
|
|
14
|
+
}
|
|
15
|
+
/** The Tier-2 projection of a rendered (post/reply) message after folding mutations in. */
|
|
16
|
+
export interface RenderedContent {
|
|
17
|
+
/** Markdown body, or `null` when deleted (tombstone) or non-text. */
|
|
18
|
+
body: string | null;
|
|
19
|
+
/** The replied-to message's content-hash, or `null`. */
|
|
20
|
+
replyTo: Uint8Array | null;
|
|
21
|
+
/** ISO timestamp of the last applied edit, or `null`. */
|
|
22
|
+
editedAt: string | null;
|
|
23
|
+
/** True when a delete tombstone has folded in. */
|
|
24
|
+
deleted: boolean;
|
|
25
|
+
/** token → count (distinct reaction messages bearing that token). */
|
|
26
|
+
reactions: Record<string, number>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stateful fold over decoded content messages. Feed every decoded message via {@link MessageFold.apply};
|
|
30
|
+
* read the rendered projection of a post/reply via {@link MessageFold.getContent}. Mutation messages
|
|
31
|
+
* (edit/delete/reaction/un-react) fold onto their target and are reported non-renderable.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const fold = new MessageFold();
|
|
36
|
+
* const { renderable } = fold.apply(decoded); // false for a reaction/edit/delete
|
|
37
|
+
* const content = fold.getContent(decoded.messageId); // post/reply projection or null
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare class MessageFold {
|
|
41
|
+
private rows;
|
|
42
|
+
private idByHash;
|
|
43
|
+
private reactionReg;
|
|
44
|
+
private pending;
|
|
45
|
+
/** Drop all state (e.g. on conversation switch or full reload before re-folding). */
|
|
46
|
+
reset(): void;
|
|
47
|
+
/**
|
|
48
|
+
* True if `messageId` produced a visible (post/reply) row.
|
|
49
|
+
* @param messageId - The server message id to test.
|
|
50
|
+
* @returns Whether a rendered row exists for it.
|
|
51
|
+
*/
|
|
52
|
+
isRenderable(messageId: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The rendered projection for a post/reply, or `null` for a folded mutation / unknown id.
|
|
55
|
+
* @param messageId - The server message id to project.
|
|
56
|
+
* @returns A fresh {@link RenderedContent} snapshot, or `null` when the id is not a rendered row.
|
|
57
|
+
*/
|
|
58
|
+
getContent(messageId: string): RenderedContent | null;
|
|
59
|
+
/**
|
|
60
|
+
* Apply one decoded message. Buffers a mutation whose target is unknown.
|
|
61
|
+
*
|
|
62
|
+
* Idempotent on a strict immediate re-apply of the same `messageId`, but NOT across an interleaved
|
|
63
|
+
* re-delivery: re-applying an already-withdrawn reaction id resurrects it (the replayed reaction
|
|
64
|
+
* re-adds itself to the now-empty token set and the prior un-react is not re-triggered). The CONSUMER
|
|
65
|
+
* (the hook) MUST dedup by `messageId` and apply each id at most once — do not re-feed an
|
|
66
|
+
* already-applied message.
|
|
67
|
+
* @param msg - The decoded, content-hashed message to fold in.
|
|
68
|
+
* @returns `{ renderable }` — whether this id is a standalone (post/reply) row.
|
|
69
|
+
*/
|
|
70
|
+
apply(msg: DecodedContentMessage): {
|
|
71
|
+
renderable: boolean;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the rendered row a content-hash points at, if it has been decoded.
|
|
75
|
+
* @param targetHash - The referenced message's content-hash.
|
|
76
|
+
* @returns The {@link Row}, or `undefined` if the target is unknown or not a rendered row.
|
|
77
|
+
*/
|
|
78
|
+
private resolveRow;
|
|
79
|
+
/**
|
|
80
|
+
* Buffer a mutation against the hex of its (not-yet-decoded) target; replayed by {@link MessageFold.drain}.
|
|
81
|
+
* @param targetHash - The target content-hash to key the buffer under.
|
|
82
|
+
* @param msg - The mutation to defer.
|
|
83
|
+
* @returns `{ renderable: false }` — a buffered mutation is never a standalone row.
|
|
84
|
+
*/
|
|
85
|
+
private buffer;
|
|
86
|
+
/**
|
|
87
|
+
* Replay every mutation buffered against a content-hash that has just been decoded.
|
|
88
|
+
* @param hashHex - The hex content-hash that just appeared (a post/reply or a now-applied reaction).
|
|
89
|
+
*/
|
|
90
|
+
private drain;
|
|
91
|
+
}
|