@agora-sdk/secure-chat-core 0.7.0 → 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 (53) 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/hooks/message-fold.d.ts +91 -0
  14. package/dist/cjs/hooks/message-fold.js +218 -0
  15. package/dist/cjs/hooks/message-fold.js.map +1 -0
  16. package/dist/cjs/hooks/useSecureMessages.d.ts +30 -18
  17. package/dist/cjs/hooks/useSecureMessages.js +190 -238
  18. package/dist/cjs/hooks/useSecureMessages.js.map +1 -1
  19. package/dist/cjs/index.d.ts +7 -0
  20. package/dist/cjs/index.js +27 -1
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/persistence/repository.d.ts +11 -11
  23. package/dist/cjs/persistence/repository.js +19 -19
  24. package/dist/cjs/persistence/repository.js.map +1 -1
  25. package/dist/cjs/version.d.ts +1 -1
  26. package/dist/cjs/version.js +1 -1
  27. package/dist/esm/content/builders.d.ts +40 -0
  28. package/dist/esm/content/builders.js +77 -0
  29. package/dist/esm/content/builders.js.map +1 -0
  30. package/dist/esm/content/cbor.d.ts +48 -0
  31. package/dist/esm/content/cbor.js +292 -0
  32. package/dist/esm/content/cbor.js.map +1 -0
  33. package/dist/esm/content/frame.d.ts +24 -0
  34. package/dist/esm/content/frame.js +34 -0
  35. package/dist/esm/content/frame.js.map +1 -0
  36. package/dist/esm/content/mimi-content.d.ts +194 -0
  37. package/dist/esm/content/mimi-content.js +283 -0
  38. package/dist/esm/content/mimi-content.js.map +1 -0
  39. package/dist/esm/hooks/message-fold.d.ts +91 -0
  40. package/dist/esm/hooks/message-fold.js +214 -0
  41. package/dist/esm/hooks/message-fold.js.map +1 -0
  42. package/dist/esm/hooks/useSecureMessages.d.ts +30 -18
  43. package/dist/esm/hooks/useSecureMessages.js +192 -240
  44. package/dist/esm/hooks/useSecureMessages.js.map +1 -1
  45. package/dist/esm/index.d.ts +7 -0
  46. package/dist/esm/index.js +6 -0
  47. package/dist/esm/index.js.map +1 -1
  48. package/dist/esm/persistence/repository.d.ts +11 -11
  49. package/dist/esm/persistence/repository.js +19 -19
  50. package/dist/esm/persistence/repository.js.map +1 -1
  51. package/dist/esm/version.d.ts +1 -1
  52. package/dist/esm/version.js +1 -1
  53. package/package.json +3 -2
@@ -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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"}
@@ -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
+ }