@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.
Files changed (89) hide show
  1. package/dist/cjs/content/builders.d.ts +40 -0
  2. package/dist/cjs/content/builders.js +85 -0
  3. package/dist/cjs/content/builders.js.map +1 -0
  4. package/dist/cjs/content/cbor.d.ts +48 -0
  5. package/dist/cjs/content/cbor.js +298 -0
  6. package/dist/cjs/content/cbor.js.map +1 -0
  7. package/dist/cjs/content/frame.d.ts +24 -0
  8. package/dist/cjs/content/frame.js +39 -0
  9. package/dist/cjs/content/frame.js.map +1 -0
  10. package/dist/cjs/content/mimi-content.d.ts +194 -0
  11. package/dist/cjs/content/mimi-content.js +289 -0
  12. package/dist/cjs/content/mimi-content.js.map +1 -0
  13. package/dist/cjs/context/secure-chat-context.d.ts +19 -3
  14. package/dist/cjs/context/secure-chat-context.js +53 -13
  15. package/dist/cjs/context/secure-chat-context.js.map +1 -1
  16. package/dist/cjs/hooks/message-fold.d.ts +91 -0
  17. package/dist/cjs/hooks/message-fold.js +218 -0
  18. package/dist/cjs/hooks/message-fold.js.map +1 -0
  19. package/dist/cjs/hooks/useSecureConversations.js +11 -0
  20. package/dist/cjs/hooks/useSecureConversations.js.map +1 -1
  21. package/dist/cjs/hooks/useSecureDevice.js +15 -1
  22. package/dist/cjs/hooks/useSecureDevice.js.map +1 -1
  23. package/dist/cjs/hooks/useSecureHandshakes.js +36 -2
  24. package/dist/cjs/hooks/useSecureHandshakes.js.map +1 -1
  25. package/dist/cjs/hooks/useSecureMessages.d.ts +30 -18
  26. package/dist/cjs/hooks/useSecureMessages.js +197 -130
  27. package/dist/cjs/hooks/useSecureMessages.js.map +1 -1
  28. package/dist/cjs/index.d.ts +8 -0
  29. package/dist/cjs/index.js +30 -1
  30. package/dist/cjs/index.js.map +1 -1
  31. package/dist/cjs/persistence/repository.d.ts +24 -0
  32. package/dist/cjs/persistence/repository.js +35 -0
  33. package/dist/cjs/persistence/repository.js.map +1 -1
  34. package/dist/cjs/persistence/store.js +11 -0
  35. package/dist/cjs/persistence/store.js.map +1 -1
  36. package/dist/cjs/transport/rest.d.ts +2 -2
  37. package/dist/cjs/transport/rest.js +2 -2
  38. package/dist/cjs/transport/rest.js.map +1 -1
  39. package/dist/cjs/transport/socket.d.ts +1 -1
  40. package/dist/cjs/transport/socket.js +17 -1
  41. package/dist/cjs/transport/socket.js.map +1 -1
  42. package/dist/cjs/version.d.ts +13 -0
  43. package/dist/cjs/version.js +19 -0
  44. package/dist/cjs/version.js.map +1 -0
  45. package/dist/esm/content/builders.d.ts +40 -0
  46. package/dist/esm/content/builders.js +77 -0
  47. package/dist/esm/content/builders.js.map +1 -0
  48. package/dist/esm/content/cbor.d.ts +48 -0
  49. package/dist/esm/content/cbor.js +292 -0
  50. package/dist/esm/content/cbor.js.map +1 -0
  51. package/dist/esm/content/frame.d.ts +24 -0
  52. package/dist/esm/content/frame.js +34 -0
  53. package/dist/esm/content/frame.js.map +1 -0
  54. package/dist/esm/content/mimi-content.d.ts +194 -0
  55. package/dist/esm/content/mimi-content.js +283 -0
  56. package/dist/esm/content/mimi-content.js.map +1 -0
  57. package/dist/esm/context/secure-chat-context.d.ts +19 -3
  58. package/dist/esm/context/secure-chat-context.js +53 -13
  59. package/dist/esm/context/secure-chat-context.js.map +1 -1
  60. package/dist/esm/hooks/message-fold.d.ts +91 -0
  61. package/dist/esm/hooks/message-fold.js +214 -0
  62. package/dist/esm/hooks/message-fold.js.map +1 -0
  63. package/dist/esm/hooks/useSecureConversations.js +11 -0
  64. package/dist/esm/hooks/useSecureConversations.js.map +1 -1
  65. package/dist/esm/hooks/useSecureDevice.js +15 -1
  66. package/dist/esm/hooks/useSecureDevice.js.map +1 -1
  67. package/dist/esm/hooks/useSecureHandshakes.js +36 -2
  68. package/dist/esm/hooks/useSecureHandshakes.js.map +1 -1
  69. package/dist/esm/hooks/useSecureMessages.d.ts +30 -18
  70. package/dist/esm/hooks/useSecureMessages.js +199 -132
  71. package/dist/esm/hooks/useSecureMessages.js.map +1 -1
  72. package/dist/esm/index.d.ts +8 -0
  73. package/dist/esm/index.js +8 -0
  74. package/dist/esm/index.js.map +1 -1
  75. package/dist/esm/persistence/repository.d.ts +24 -0
  76. package/dist/esm/persistence/repository.js +35 -0
  77. package/dist/esm/persistence/repository.js.map +1 -1
  78. package/dist/esm/persistence/store.js +11 -0
  79. package/dist/esm/persistence/store.js.map +1 -1
  80. package/dist/esm/transport/rest.d.ts +2 -2
  81. package/dist/esm/transport/rest.js +2 -2
  82. package/dist/esm/transport/rest.js.map +1 -1
  83. package/dist/esm/transport/socket.d.ts +1 -1
  84. package/dist/esm/transport/socket.js +17 -1
  85. package/dist/esm/transport/socket.js.map +1 -1
  86. package/dist/esm/version.d.ts +13 -0
  87. package/dist/esm/version.js +16 -0
  88. package/dist/esm/version.js.map +1 -0
  89. package/package.json +3 -3
@@ -0,0 +1,194 @@
1
+ import { type CborMap } from "./cbor.js";
2
+ /**
3
+ * Part cardinality tag. Values per draft-ietf-mimi-content-08
4
+ * (`nullpart=0, single=1, external=2, multi=3`).
5
+ */
6
+ export declare const Cardinality: {
7
+ readonly Null: 0;
8
+ readonly Single: 1;
9
+ readonly External: 2;
10
+ readonly Multi: 3;
11
+ };
12
+ /** The set of valid {@link Cardinality} tag values. */
13
+ export type Cardinality = (typeof Cardinality)[keyof typeof Cardinality];
14
+ /**
15
+ * Part disposition (draft-ietf-mimi-content-08 `baseDispos`). `Reaction` distinguishes a reaction
16
+ * from a reply; `Attachment`/`Inline`/etc. drive client rendering.
17
+ */
18
+ export declare const Disposition: {
19
+ readonly Unspecified: 0;
20
+ readonly Render: 1;
21
+ readonly Reaction: 2;
22
+ readonly Profile: 3;
23
+ readonly Inline: 4;
24
+ readonly Icon: 5;
25
+ readonly Attachment: 6;
26
+ readonly Session: 7;
27
+ readonly Preview: 8;
28
+ };
29
+ /** The set of valid {@link Disposition} values. */
30
+ export type Disposition = (typeof Disposition)[keyof typeof Disposition];
31
+ /**
32
+ * {@link MultiPart} `partSemantics` — how a client treats the child parts (draft-ietf-mimi-content-08:
33
+ * `chooseOne=0, singleUnit=1, processAll=2`).
34
+ */
35
+ export declare const PartSemantics: {
36
+ readonly ChooseOne: 0;
37
+ readonly SingleUnit: 1;
38
+ readonly ProcessAll: 2;
39
+ };
40
+ /** The set of valid {@link PartSemantics} values. */
41
+ export type PartSemantics = (typeof PartSemantics)[keyof typeof PartSemantics];
42
+ /**
43
+ * Named-Information hash algorithm. SHA-256 is the only value we emit (identifier `0x01`, per
44
+ * draft-ietf-mimi-content-08).
45
+ */
46
+ export declare const HashAlg: {
47
+ readonly Sha256: 1;
48
+ };
49
+ /** The set of valid {@link HashAlg} values. */
50
+ export type HashAlg = (typeof HashAlg)[keyof typeof HashAlg];
51
+ /** A tombstone body (delete / un-react) — `NullPart` with the NestedPart wrapper's disposition/language. */
52
+ export interface NullPart {
53
+ /** Cardinality tag — always {@link Cardinality.Null}. */
54
+ cardinality: typeof Cardinality.Null;
55
+ /** Wrapper disposition (typically {@link Disposition.Render}); kept for wire-faithful round-trip. */
56
+ disposition: Disposition;
57
+ /** BCP-47 language tag, or `""`. */
58
+ language: string;
59
+ }
60
+ /** An inline body part (text, reaction token, …) — draft `SinglePart` + the NestedPart wrapper fields. */
61
+ export interface SinglePart {
62
+ /** Cardinality tag — always {@link Cardinality.Single}. */
63
+ cardinality: typeof Cardinality.Single;
64
+ /** How a client should treat this part (`Render` for text, `Reaction` for a reaction token, …). */
65
+ disposition: Disposition;
66
+ /** BCP-47 language tag for `content`, or `""` when not applicable. */
67
+ language: string;
68
+ /** MIME type of `content` (e.g. `text/markdown`). */
69
+ contentType: string;
70
+ /** The raw body bytes (UTF-8 for text types). */
71
+ content: Uint8Array;
72
+ }
73
+ /** An external (by-reference, encrypted) attachment part — Tier 3; encoded, not yet surfaced. */
74
+ export interface ExternalPart {
75
+ /** Cardinality tag — always {@link Cardinality.External}. */
76
+ cardinality: typeof Cardinality.External;
77
+ /** How a client should treat this part (typically {@link Disposition.Attachment}). */
78
+ disposition: Disposition;
79
+ /** BCP-47 language tag, or `""`. */
80
+ language: string;
81
+ /** MIME type of the referenced blob. */
82
+ contentType: string;
83
+ /** Fetch URL for the encrypted blob. */
84
+ url: string;
85
+ /** Absolute expiry of the URL (0 = none). */
86
+ expires: number;
87
+ /** Plaintext size in bytes. */
88
+ size: number;
89
+ /** Symmetric encryption algorithm identifier for the blob (draft `encAlg`). */
90
+ encAlgorithm: number;
91
+ /** Symmetric key for the blob (kept client-side only). */
92
+ key: Uint8Array;
93
+ /** AEAD nonce. */
94
+ nonce: Uint8Array;
95
+ /** AEAD associated data. */
96
+ aad: Uint8Array;
97
+ /** Hash algorithm of {@link ExternalPart.contentHash} (we emit {@link HashAlg.Sha256}). */
98
+ hashAlgorithm: HashAlg;
99
+ /** Hash of the plaintext blob (integrity). */
100
+ contentHash: Uint8Array;
101
+ /** Human-readable description / alt text. */
102
+ description: string;
103
+ /** Suggested filename. */
104
+ filename: string;
105
+ }
106
+ /** A composite body of ordered sub-parts (draft `MultiPart`; at least 2 children). */
107
+ export interface MultiPart {
108
+ /** Cardinality tag — always {@link Cardinality.Multi}. */
109
+ cardinality: typeof Cardinality.Multi;
110
+ /** Wrapper disposition for the composite. */
111
+ disposition: Disposition;
112
+ /** BCP-47 language tag, or `""`. */
113
+ language: string;
114
+ /** How the child parts relate ({@link PartSemantics}). */
115
+ partSemantics: PartSemantics;
116
+ /** The ordered child parts (draft requires 2 or more). */
117
+ parts: Part[];
118
+ }
119
+ /** The body tree: a cardinality-tagged union of {@link NullPart}, {@link SinglePart}, {@link ExternalPart}, {@link MultiPart}. */
120
+ export type Part = NullPart | SinglePart | ExternalPart | MultiPart;
121
+ /** Expiry directive (draft `Expiration = [relative: bool, time: uint .size 4]`). */
122
+ export interface Expiration {
123
+ /** `true` = `time` is seconds relative to receipt; `false` = absolute UNIX time. */
124
+ relative: boolean;
125
+ /** The expiry time (seconds). */
126
+ time: number;
127
+ }
128
+ /** The MIMI message content structure (draft-ietf-mimi-content-08; see file header for the CDDL). */
129
+ export interface MimiContent {
130
+ /** Per-message CSPRNG bytes (draft `salt`, 16 bytes) — unlinkability of the content hash. */
131
+ salt: Uint8Array;
132
+ /** 32-byte content-hash (MessageId) of a message this replaces (edit/delete/un-react), or `null`. */
133
+ replaces: Uint8Array | null;
134
+ /** Threading topic id (Tier 3; encoded, unsurfaced). Empty = none. */
135
+ topicId: Uint8Array;
136
+ /** Expiry directive (draft `Expiration`), or `null` for none. Tier 3; encoded, unsurfaced. */
137
+ expires: Expiration | null;
138
+ /** 32-byte content-hash (MessageId) of the reply target, or `null`. */
139
+ inReplyTo: Uint8Array | null;
140
+ /** Extension map (round-tripped opaquely). */
141
+ extensions: CborMap;
142
+ /** The message body tree (draft `NestedPart`). */
143
+ nestedPart: Part;
144
+ }
145
+ /**
146
+ * Schema bounds enforced on decode (DoS / abuse guard): max sibling parts in a {@link MultiPart}, and
147
+ * max part-tree depth. (draft-08 has no `lastSeen`, so there is no list bound here.)
148
+ */
149
+ export declare const MIMI_LIMITS: {
150
+ readonly maxParts: 64;
151
+ readonly maxNesting: 8;
152
+ };
153
+ /**
154
+ * Encode a {@link MimiContent} to canonical CBOR bytes per the draft-08 wire layout.
155
+ * @param c - The content to encode.
156
+ * @returns Canonical CBOR bytes (stable for equal content — the basis for {@link contentHash}).
157
+ * @throws {Error} If a field carries an out-of-subset CBOR value (e.g. a non-integer number).
158
+ * @example
159
+ * ```ts
160
+ * const bytes = encodeMimiContent(myContent);
161
+ * const same = decodeMimiContent(bytes); // round-trips
162
+ * ```
163
+ */
164
+ export declare function encodeMimiContent(c: MimiContent): Uint8Array;
165
+ /**
166
+ * Strict-decode + schema-validate canonical CBOR into a {@link MimiContent}. Fails closed on any
167
+ * structural or bounds violation (untrusted peer input — CLAUDE.md §1).
168
+ * @param bytes - The canonical CBOR content bytes (after unframing).
169
+ * @returns The validated {@link MimiContent}.
170
+ * @throws {Error} On a malformed structure, wrong field type, an unknown {@link Cardinality}, or a
171
+ * {@link MIMI_LIMITS} bound exceeded. The error message never includes message plaintext.
172
+ * @example
173
+ * ```ts
174
+ * const content = decodeMimiContent(peerBytes); // throws on any tampering
175
+ * ```
176
+ */
177
+ export declare function decodeMimiContent(bytes: Uint8Array): MimiContent;
178
+ /**
179
+ * The MIMI content-hash: a 32-byte, MessageId-SHAPED reference value used to point at a message
180
+ * (reply / edit / delete / un-react). Per the file-header deviation note, we lack federation
181
+ * identities, so instead of draft-08's URI-prefixed input we compute
182
+ * `0x01 || sha256(encodeMimiContent(c))[0..30]` — the leading `0x01` is the SHA-256 hash-algorithm
183
+ * identifier, then the first 31 bytes of the SHA-256 over our canonical CBOR (which already includes
184
+ * the per-message `salt`). Structurally a real MIMI MessageId; stable across runtimes; salt-sensitive.
185
+ * The blind server never sees this. `replaces` / `inReplyTo` are populated with this value.
186
+ * @param c - The content to hash.
187
+ * @returns A 32-byte MessageId (`[0]` is `0x01`; `[1..31]` are `sha256(canonical CBOR)[0..30]`).
188
+ * @example
189
+ * ```ts
190
+ * const id = contentHash(myContent); // 32 bytes, id[0] === 0x01, salt-sensitive
191
+ * reply.inReplyTo = id;
192
+ * ```
193
+ */
194
+ export declare function contentHash(c: MimiContent): Uint8Array;
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ // MimiContent — secure-chat's message content format. IMPLEMENTS draft-ietf-mimi-content-08 (2 Mar
3
+ // 2026), the message-content CDDL. The CBOR WIRE STRUCTURE below is draft-08-faithful with NO deviation
4
+ // (the interop-readiness payoff); the ONE deliberate deviation is MessageId DERIVATION (we lack
5
+ // federation identities) — documented in full at the bottom of this header.
6
+ //
7
+ // Where it sits: pure structure ABOVE the SecureChatCrypto seam and INSIDE the padding frame. Content
8
+ // is a core concern, never crypto's. We encode/decode the FULL draft structure (so Tier-3 fields
9
+ // round-trip today and surface later without rework); the hook surfaces Tier 2 (text / reply /
10
+ // reaction / edit / delete). Decoded bytes are UNTRUSTED peer input: decodeMimiContent validates the
11
+ // schema AFTER canonical CBOR-decoding and fails closed (CLAUDE.md §1).
12
+ //
13
+ // Wire CDDL (draft-ietf-mimi-content-08, the message-content section) — exactly what we encode/decode:
14
+ //
15
+ // mimiContent = [salt:bstr.size 16, replaces:null/MessageId, topicId:bstr,
16
+ // expires:null/Expiration, inReplyTo:null/MessageId,
17
+ // mimiExtensions:extensions, nestedPart:NestedPart] ; 7 elements
18
+ // MessageId = bstr.size 32
19
+ // Expiration = [relative:bool, time:uint.size 4]
20
+ // extensions = { ? &(senderUri:1)^ => tstr, ? &(roomUri:2)^ => tstr, * otherKnown, * unknown }
21
+ // name = int / tstr.size (1..255) value = any.size (0..4095)
22
+ // NestedPart = [disposition, language:tstr, (NullPart // SinglePart // ExternalPart // MultiPart)]
23
+ // NullPart = (cardinality:nullpart)
24
+ // SinglePart = (cardinality:single, contentType:tstr, content:bstr)
25
+ // ExternalPart= (cardinality:external, contentType:tstr, url:tstr, expires:uint.size 4,
26
+ // size:uint.size 8, encAlg:uint.size 2, key:bstr, nonce:bstr, aad:bstr,
27
+ // hashAlg:uint.size 1, contentHash:bstr, description:tstr, filename:tstr)
28
+ // MultiPart = (cardinality:multi, partSemantics, parts:[2* NestedPart]) ; ≥2 parts
29
+ // baseDispos: unspecified=0 render=1 reaction=2 profile=3 inline=4 icon=5 attachment=6 session=7 preview=8
30
+ // cardinality: nullpart=0 single=1 external=2 multi=3 partSemantics: chooseOne=0 singleUnit=1 processAll=2
31
+ // SHA-256 hash-algorithm identifier = 0x01
32
+ //
33
+ // Because NestedPart hoists `disposition` + `language` OUT of the variant, a single part encodes to the
34
+ // CBOR array [disposition, language, cardinality, …variantFields]. Our TS `Part` types keep
35
+ // `disposition`/`language` ON each variant (so the reducer can branch on `part.disposition ===
36
+ // Disposition.Reaction` and builders can set it); the codec maps them to/from the wrapper positions.
37
+ //
38
+ // ── THE ONE DELIBERATE DEVIATION: MessageId derivation ───────────────────────────────────────────
39
+ // draft-08 derives a MessageId from FEDERATION IDENTITIES:
40
+ // messageId = 0x01 || SHA256(senderUriLen || senderUri || roomUriLen || roomUri || message || salt)[0..30]
41
+ // This SDK adopts the MIMI content FORMAT, not MIMI federation (design spec Goal A) — we have no
42
+ // senderUri / roomUri. So `contentHash(content)` returns a 32-byte MessageId-SHAPED value:
43
+ // 0x01 (SHA-256 hashAlg) || sha256(encodeMimiContent(content))[0..30] (1 + 31 = 32 bytes)
44
+ // — structurally a real MIMI MessageId; only the hash INPUT is simplified (our canonical CBOR, which
45
+ // already includes the per-message `salt`, with no URI prefixes). `replaces`/`inReplyTo` carry THIS
46
+ // value. This is the single upgrade-when-federation-lands deviation; the WIRE CBOR has none.
47
+ //
48
+ // Design-doc reconcile (FOLLOWING THE DRAFT; for the doc owner): the doc's top-level `lastSeen` list,
49
+ // the per-Part `partIndex`, and `width`/`height`/`duration` on ExternalPart are NOT in draft-08 and are
50
+ // DROPPED; the doc modeled `inReplyTo` as {hash, hashAlgorithm} but the draft uses a bare 32-byte
51
+ // MessageId, so `inReplyTo` is now `Uint8Array | null` and the `MessageDerivedValue` type is removed.
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.MIMI_LIMITS = exports.HashAlg = exports.PartSemantics = exports.Disposition = exports.Cardinality = void 0;
54
+ exports.encodeMimiContent = encodeMimiContent;
55
+ exports.decodeMimiContent = decodeMimiContent;
56
+ exports.contentHash = contentHash;
57
+ const cbor_js_1 = require("./cbor.js");
58
+ const sha2_js_1 = require("@noble/hashes/sha2.js");
59
+ /**
60
+ * Part cardinality tag. Values per draft-ietf-mimi-content-08
61
+ * (`nullpart=0, single=1, external=2, multi=3`).
62
+ */
63
+ exports.Cardinality = { Null: 0, Single: 1, External: 2, Multi: 3 };
64
+ /**
65
+ * Part disposition (draft-ietf-mimi-content-08 `baseDispos`). `Reaction` distinguishes a reaction
66
+ * from a reply; `Attachment`/`Inline`/etc. drive client rendering.
67
+ */
68
+ exports.Disposition = {
69
+ Unspecified: 0, Render: 1, Reaction: 2, Profile: 3, Inline: 4, Icon: 5,
70
+ Attachment: 6, Session: 7, Preview: 8,
71
+ };
72
+ /**
73
+ * {@link MultiPart} `partSemantics` — how a client treats the child parts (draft-ietf-mimi-content-08:
74
+ * `chooseOne=0, singleUnit=1, processAll=2`).
75
+ */
76
+ exports.PartSemantics = { ChooseOne: 0, SingleUnit: 1, ProcessAll: 2 };
77
+ /**
78
+ * Named-Information hash algorithm. SHA-256 is the only value we emit (identifier `0x01`, per
79
+ * draft-ietf-mimi-content-08).
80
+ */
81
+ exports.HashAlg = { Sha256: 1 };
82
+ /**
83
+ * Schema bounds enforced on decode (DoS / abuse guard): max sibling parts in a {@link MultiPart}, and
84
+ * max part-tree depth. (draft-08 has no `lastSeen`, so there is no list bound here.)
85
+ */
86
+ exports.MIMI_LIMITS = { maxParts: 64, maxNesting: 8 };
87
+ // ── encode ────────────────────────────────────────────────────────────────────
88
+ function encodePart(p) {
89
+ // NestedPart = [disposition, language, …variant-starting-with-cardinality].
90
+ switch (p.cardinality) {
91
+ case exports.Cardinality.Null:
92
+ return [p.disposition, p.language, exports.Cardinality.Null];
93
+ case exports.Cardinality.Single:
94
+ return [p.disposition, p.language, exports.Cardinality.Single, p.contentType, p.content];
95
+ case exports.Cardinality.External:
96
+ return [
97
+ p.disposition, p.language, exports.Cardinality.External, p.contentType, p.url, p.expires, p.size,
98
+ p.encAlgorithm, p.key, p.nonce, p.aad, p.hashAlgorithm, p.contentHash, p.description, p.filename,
99
+ ];
100
+ case exports.Cardinality.Multi:
101
+ return [p.disposition, p.language, exports.Cardinality.Multi, p.partSemantics, p.parts.map(encodePart)];
102
+ }
103
+ }
104
+ function toCbor(c) {
105
+ return [
106
+ c.salt,
107
+ c.replaces, // bstr(32) | null
108
+ c.topicId,
109
+ c.expires ? [c.expires.relative, c.expires.time] : null, // Expiration | null
110
+ c.inReplyTo, // bstr(32) | null
111
+ c.extensions,
112
+ encodePart(c.nestedPart),
113
+ ];
114
+ }
115
+ /**
116
+ * Encode a {@link MimiContent} to canonical CBOR bytes per the draft-08 wire layout.
117
+ * @param c - The content to encode.
118
+ * @returns Canonical CBOR bytes (stable for equal content — the basis for {@link contentHash}).
119
+ * @throws {Error} If a field carries an out-of-subset CBOR value (e.g. a non-integer number).
120
+ * @example
121
+ * ```ts
122
+ * const bytes = encodeMimiContent(myContent);
123
+ * const same = decodeMimiContent(bytes); // round-trips
124
+ * ```
125
+ */
126
+ function encodeMimiContent(c) {
127
+ return (0, cbor_js_1.encode)(toCbor(c));
128
+ }
129
+ // ── decode + validate ───────────────────────────────────────────────────────
130
+ function asArray(v, ctx) {
131
+ if (!Array.isArray(v))
132
+ throw new Error(`MimiContent: expected array (${ctx})`);
133
+ return v;
134
+ }
135
+ function asBytes(v, ctx) {
136
+ if (!(v instanceof Uint8Array))
137
+ throw new Error(`MimiContent: expected bytes (${ctx})`);
138
+ return v;
139
+ }
140
+ function asString(v, ctx) {
141
+ if (typeof v !== "string")
142
+ throw new Error(`MimiContent: expected string (${ctx})`);
143
+ return v;
144
+ }
145
+ function asInt(v, ctx) {
146
+ if (typeof v !== "number" || !Number.isInteger(v))
147
+ throw new Error(`MimiContent: expected int (${ctx})`);
148
+ return v;
149
+ }
150
+ function asBool(v, ctx) {
151
+ if (typeof v !== "boolean")
152
+ throw new Error(`MimiContent: expected bool (${ctx})`);
153
+ return v;
154
+ }
155
+ /**
156
+ * Exact wire-array length per cardinality (`[disposition, language, cardinality, …variant]`). Decode
157
+ * asserts this exactly so a part carrying TRAILING junk is rejected, not silently dropped (CLAUDE.md
158
+ * §1: fail closed — never skip the check).
159
+ */
160
+ const PART_ARITY = {
161
+ [exports.Cardinality.Null]: 3, // disposition, language, cardinality
162
+ [exports.Cardinality.Single]: 5, // + contentType, content
163
+ // disposition, language, cardinality + 12 ExternalPart fields (contentType, url, expires, size,
164
+ // encAlg, key, nonce, aad, hashAlg, contentHash, description, filename) = 15. (decode reads a[14].)
165
+ [exports.Cardinality.External]: 15,
166
+ [exports.Cardinality.Multi]: 5, // + partSemantics, parts
167
+ };
168
+ function decodePart(v, depth) {
169
+ if (depth > exports.MIMI_LIMITS.maxNesting)
170
+ throw new Error("MimiContent: part nesting exceeds bounds");
171
+ const a = asArray(v, "part");
172
+ // NestedPart wrapper: [disposition, language, cardinality, …variant].
173
+ const disposition = asInt(a[0], "part.disposition");
174
+ const language = asString(a[1], "part.language");
175
+ const card = asInt(a[2], "part.cardinality");
176
+ // Fail closed on an unknown cardinality OR a wrong-length part array (trailing/missing elements).
177
+ const expectedArity = PART_ARITY[card];
178
+ if (expectedArity === undefined)
179
+ throw new Error(`MimiContent: unknown part cardinality ${card}`);
180
+ if (a.length !== expectedArity)
181
+ throw new Error(`MimiContent: wrong arity for cardinality ${card}`);
182
+ switch (card) {
183
+ case exports.Cardinality.Null:
184
+ return { cardinality: exports.Cardinality.Null, disposition, language };
185
+ case exports.Cardinality.Single:
186
+ return {
187
+ cardinality: exports.Cardinality.Single,
188
+ disposition,
189
+ language,
190
+ contentType: asString(a[3], "contentType"),
191
+ content: asBytes(a[4], "content"),
192
+ };
193
+ case exports.Cardinality.External:
194
+ return {
195
+ cardinality: exports.Cardinality.External,
196
+ disposition,
197
+ language,
198
+ contentType: asString(a[3], "contentType"),
199
+ url: asString(a[4], "url"),
200
+ expires: asInt(a[5], "expires"),
201
+ size: asInt(a[6], "size"),
202
+ encAlgorithm: asInt(a[7], "encAlgorithm"),
203
+ key: asBytes(a[8], "key"),
204
+ nonce: asBytes(a[9], "nonce"),
205
+ aad: asBytes(a[10], "aad"),
206
+ hashAlgorithm: asInt(a[11], "hashAlgorithm"),
207
+ contentHash: asBytes(a[12], "contentHash"),
208
+ description: asString(a[13], "description"),
209
+ filename: asString(a[14], "filename"),
210
+ };
211
+ case exports.Cardinality.Multi: {
212
+ const parts = asArray(a[4], "multi.parts");
213
+ // The draft requires 2 or more child parts; reject a degenerate multipart fail-closed.
214
+ if (parts.length < 2)
215
+ throw new Error("MimiContent: multipart requires 2+ parts");
216
+ if (parts.length > exports.MIMI_LIMITS.maxParts)
217
+ throw new Error("MimiContent: too many parts (bounds)");
218
+ return {
219
+ cardinality: exports.Cardinality.Multi,
220
+ disposition,
221
+ language,
222
+ partSemantics: asInt(a[3], "partSemantics"),
223
+ parts: parts.map((p) => decodePart(p, depth + 1)),
224
+ };
225
+ }
226
+ default:
227
+ throw new Error(`MimiContent: unknown part cardinality ${card}`);
228
+ }
229
+ }
230
+ /**
231
+ * Strict-decode + schema-validate canonical CBOR into a {@link MimiContent}. Fails closed on any
232
+ * structural or bounds violation (untrusted peer input — CLAUDE.md §1).
233
+ * @param bytes - The canonical CBOR content bytes (after unframing).
234
+ * @returns The validated {@link MimiContent}.
235
+ * @throws {Error} On a malformed structure, wrong field type, an unknown {@link Cardinality}, or a
236
+ * {@link MIMI_LIMITS} bound exceeded. The error message never includes message plaintext.
237
+ * @example
238
+ * ```ts
239
+ * const content = decodeMimiContent(peerBytes); // throws on any tampering
240
+ * ```
241
+ */
242
+ function decodeMimiContent(bytes) {
243
+ const a = asArray((0, cbor_js_1.decode)(bytes, { maxItems: exports.MIMI_LIMITS.maxParts * 4 }), "MimiContent");
244
+ if (a.length !== 7)
245
+ throw new Error("MimiContent: wrong top-level arity");
246
+ const replaces = a[1] === null ? null : asBytes(a[1], "replaces");
247
+ let expires = null;
248
+ if (a[3] !== null) {
249
+ const e = asArray(a[3], "expires");
250
+ expires = { relative: asBool(e[0], "expires.relative"), time: asInt(e[1], "expires.time") };
251
+ }
252
+ const inReplyTo = a[4] === null ? null : asBytes(a[4], "inReplyTo");
253
+ const ext = a[5];
254
+ if (!(ext instanceof Map))
255
+ throw new Error("MimiContent: extensions must be a map");
256
+ return {
257
+ salt: asBytes(a[0], "salt"),
258
+ replaces,
259
+ topicId: asBytes(a[2], "topicId"),
260
+ expires,
261
+ inReplyTo,
262
+ extensions: ext,
263
+ nestedPart: decodePart(a[6], 0),
264
+ };
265
+ }
266
+ /**
267
+ * The MIMI content-hash: a 32-byte, MessageId-SHAPED reference value used to point at a message
268
+ * (reply / edit / delete / un-react). Per the file-header deviation note, we lack federation
269
+ * identities, so instead of draft-08's URI-prefixed input we compute
270
+ * `0x01 || sha256(encodeMimiContent(c))[0..30]` — the leading `0x01` is the SHA-256 hash-algorithm
271
+ * identifier, then the first 31 bytes of the SHA-256 over our canonical CBOR (which already includes
272
+ * the per-message `salt`). Structurally a real MIMI MessageId; stable across runtimes; salt-sensitive.
273
+ * The blind server never sees this. `replaces` / `inReplyTo` are populated with this value.
274
+ * @param c - The content to hash.
275
+ * @returns A 32-byte MessageId (`[0]` is `0x01`; `[1..31]` are `sha256(canonical CBOR)[0..30]`).
276
+ * @example
277
+ * ```ts
278
+ * const id = contentHash(myContent); // 32 bytes, id[0] === 0x01, salt-sensitive
279
+ * reply.inReplyTo = id;
280
+ * ```
281
+ */
282
+ function contentHash(c) {
283
+ const digest = (0, sha2_js_1.sha256)(encodeMimiContent(c));
284
+ const messageId = new Uint8Array(32);
285
+ messageId[0] = exports.HashAlg.Sha256; // 0x01 — the MessageId hashAlg prefix byte
286
+ messageId.set(digest.subarray(0, 31), 1); // first 31 SHA-256 bytes fill [1..31]
287
+ return messageId;
288
+ }
289
+ //# 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
- /** Override the API base URL. Defaults to @agora-sdk/core `getApiBaseUrl()`. */
56
- baseUrl?: string;
57
- /** Override the socket origin. Defaults to @agora-sdk/core `getSocketUrl()`. */
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
@@ -5,17 +5,21 @@ exports.useSecureChat = useSecureChat;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  // SecureChatProvider — wires transport + crypto + persistence for the secure-chat hooks.
7
7
  //
8
- // Sits INSIDE a ReplykeProvider: by default it resolves the API base URL and socket origin from
9
- // @agora-sdk/core's runtime singletons (getApiBaseUrl / getSocketUrl). Crypto AND the persistence
10
- // store are injected, keeping core platform- and library-agnostic. The provider builds a typed
11
- // SecureChatRepository over the store plus a cached resolveGroup/rememberGroup so the hooks become
12
- // self-sufficient (no need to thread a GroupHandle in by hand).
8
+ // Standalone transport: the caller supplies the API base URL (and optional socket origin) directly —
9
+ // this package has NO dependency on @agora-sdk/core. (It used to fall back to core's getApiBaseUrl /
10
+ // getSocketUrl runtime singletons; that coupling existed only to auto-inherit a Replyke app's config,
11
+ // and core's actual surface here was just two URL accessors. Requiring `baseUrl` makes secure chat a
12
+ // self-contained E2EE transport usable in any app.) Crypto AND the persistence store are injected too,
13
+ // keeping this layer platform- and library-agnostic. The provider builds a typed SecureChatRepository
14
+ // over the store plus a cached resolveGroup/rememberGroup so the hooks become self-sufficient (no need
15
+ // to thread a GroupHandle in by hand).
13
16
  const react_1 = require("react");
14
- const core_1 = require("@agora-sdk/core");
15
17
  const rest_js_1 = require("../transport/rest.js");
16
18
  const socket_js_1 = require("../transport/socket.js");
17
19
  const memory_store_js_1 = require("../persistence/memory-store.js");
18
20
  const repository_js_1 = require("../persistence/repository.js");
21
+ const debug_js_1 = require("../util/debug.js");
22
+ const log = (0, debug_js_1.createDebugLogger)("provider");
19
23
  const SecureChatContext = (0, react_1.createContext)(null);
20
24
  /**
21
25
  * Provides secure-chat transport, crypto, and persistence to the `useSecure*` hooks. Render inside a
@@ -38,13 +42,14 @@ function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessTo
38
42
  const rest = (0, react_1.useMemo)(() => new rest_js_1.SecureChatRestClient({
39
43
  projectId,
40
44
  getAccessToken: resolveToken,
41
- getBaseUrl: () => baseUrl ?? (0, core_1.getApiBaseUrl)(),
45
+ getBaseUrl: () => baseUrl,
42
46
  }), [projectId, resolveToken, baseUrl]);
43
47
  const socket = (0, react_1.useMemo)(() => new socket_js_1.SecureChatSocketClient({
44
48
  projectId,
45
49
  getAccessToken: resolveToken,
46
- getSocketUrl: () => socketUrl ?? (0, core_1.getSocketUrl)(),
47
- }), [projectId, resolveToken, socketUrl]);
50
+ // Socket origin defaults to the REST base URL (the client strips the path to an origin).
51
+ getSocketUrl: () => socketUrl ?? baseUrl,
52
+ }), [projectId, resolveToken, socketUrl, baseUrl]);
48
53
  const resolvedStore = (0, react_1.useMemo)(() => store ?? new memory_store_js_1.MemoryStore(), [store]);
49
54
  const repo = (0, react_1.useMemo)(() => new repository_js_1.SecureChatRepository(resolvedStore), [resolvedStore]);
50
55
  // In-memory GroupHandle cache, keyed by conversationId. Survives re-renders via the ref.
@@ -62,11 +67,33 @@ function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessTo
62
67
  if (cached)
63
68
  return cached;
64
69
  const bytes = await repo.loadGroupState(conversationId);
65
- if (!bytes)
70
+ if (!bytes) {
71
+ // No persisted group for this conversation: the recipient never joined (no Welcome processed),
72
+ // or local state was wiped. Renders as "waiting for key update" until a Welcome arrives.
73
+ log.debug("resolveGroup: no persisted group state", { conversationId });
66
74
  return null;
67
- const handle = await crypto.importGroupState(bytes);
68
- groupCache.current.set(conversationId, handle);
69
- return handle;
75
+ }
76
+ try {
77
+ const handle = await crypto.importGroupState(bytes);
78
+ groupCache.current.set(conversationId, handle);
79
+ log.debug("resolveGroup: imported group from store", {
80
+ conversationId,
81
+ epoch: handle.epoch.toString(),
82
+ bytes: bytes.length,
83
+ });
84
+ return handle;
85
+ }
86
+ catch (err) {
87
+ // A persisted group that can't be re-imported is a black hole — it silently degrades to "no
88
+ // group" (a permanent "waiting for key update"). Surface it loudly so a reload-persistence or
89
+ // crypto-version-skew problem is visible instead of looking like an un-joined conversation.
90
+ log.debug("resolveGroup: importGroupState FAILED — group present but unreadable", {
91
+ conversationId,
92
+ bytes: bytes.length,
93
+ error: err instanceof Error ? err.message : String(err),
94
+ });
95
+ throw err;
96
+ }
70
97
  }, [repo, crypto]);
71
98
  const rememberGroup = (0, react_1.useCallback)(async (conversationId, handle) => {
72
99
  groupCache.current.set(conversationId, handle);
@@ -76,6 +103,17 @@ function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessTo
76
103
  groupVersion.current.set(conversationId, (groupVersion.current.get(conversationId) ?? 0) + 1);
77
104
  groupListeners.current.forEach((l) => l());
78
105
  }, [repo, crypto]);
106
+ const persistGroupState = (0, react_1.useCallback)(async (conversationId, handle) => {
107
+ // Re-export the now-advanced ratchet for THIS group and overwrite the persisted blob. The
108
+ // ratchet state lives in the crypto's internal per-group map (keyed by mlsGroupId), so exporting
109
+ // the same handle after a send/receive captures the advanced generation. Deliberately NO version
110
+ // bump and NO listener notify (cf. rememberGroup): an application message is intra-epoch, so a
111
+ // re-resolve would be wasted work and could churn buffered-row retries. The cache holds the same
112
+ // handle object the hook already uses, so re-setting it is idempotent.
113
+ groupCache.current.set(conversationId, handle);
114
+ const bytes = await crypto.exportGroupState(handle);
115
+ await repo.saveGroupState(conversationId, bytes);
116
+ }, [repo, crypto]);
79
117
  const getGroupVersion = (0, react_1.useCallback)((conversationId) => groupVersion.current.get(conversationId) ?? 0, []);
80
118
  const subscribeGroupChange = (0, react_1.useCallback)((listener) => {
81
119
  groupListeners.current.add(listener);
@@ -93,6 +131,7 @@ function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessTo
93
131
  repo,
94
132
  resolveGroup,
95
133
  rememberGroup,
134
+ persistGroupState,
96
135
  getGroupVersion,
97
136
  subscribeGroupChange,
98
137
  padding,
@@ -104,6 +143,7 @@ function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessTo
104
143
  repo,
105
144
  resolveGroup,
106
145
  rememberGroup,
146
+ persistGroupState,
107
147
  getGroupVersion,
108
148
  subscribeGroupChange,
109
149
  padding,