@hraness/message-like-me 0.8.3 → 0.8.5

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.
@@ -1,26 +1,644 @@
1
1
  // @bun
2
2
  import {
3
- LOCAL_MESSAGE_BUNDLE_V2_ARTIFACTS,
4
- LOCAL_MESSAGE_BUNDLE_V2_FORMAT,
5
- LOCAL_MESSAGE_BUNDLE_V2_LIMITS,
6
- LOCAL_MESSAGE_BUNDLE_V2_NETWORK,
7
- LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_ID,
8
- LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_VERSION,
9
- LOCAL_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
10
- LOCAL_MESSAGE_BUNDLE_V2_SOURCE_ID,
11
- LOCAL_MESSAGE_BUNDLE_V2_SOURCE_TRANSFORM_VERSION,
12
- LOCAL_MESSAGE_BUNDLE_V2_SUPPORTED_SOURCE_TRANSFORM_VERSIONS,
13
- MessageBundleV2ContractError,
14
- assertLocalMessageBundleV2SourceTransformVersion,
15
- isLocalMessageBundleV2SourceTransformVersion,
16
- localMessageBundleV2BundleSha256,
17
- localMessageBundleV2ManifestProjection,
18
- parseLocalMessageBundleV2Manifest,
19
- parseLocalMessageBundleV2Record,
20
- parseLocalMessageBundleV2WhatsAppJid
21
- } from "./cli-bs3db5jr.js";
22
- import"./cli-kw20gkk3.js";
23
- import"./cli-ththzwja.js";
3
+ MESSAGE_BUNDLE_V2_SCHEMA_IDENTITY
4
+ } from "./index-kw20gkk3.js";
5
+ import {
6
+ canonicalJson,
7
+ sha256
8
+ } from "./index-y1aec88m.js";
9
+
10
+ // src/message-bundle-v2.ts
11
+ import { types as nodeTypes } from "util";
12
+ var LOCAL_MESSAGE_BUNDLE_V2_SCHEMA_VERSION = MESSAGE_BUNDLE_V2_SCHEMA_IDENTITY;
13
+ var LOCAL_MESSAGE_BUNDLE_V2_FORMAT = "message-like-me.local-message-bundle";
14
+ var LOCAL_MESSAGE_BUNDLE_V2_SOURCE_ID = "wacli-local";
15
+ var LOCAL_MESSAGE_BUNDLE_V2_SOURCE_TRANSFORM_VERSION = "1.0.0";
16
+ var LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_ID = "whatsapp";
17
+ var LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_VERSION = "0.15.0";
18
+ var LOCAL_MESSAGE_BUNDLE_V2_NETWORK = "whatsapp";
19
+ var LOCAL_MESSAGE_BUNDLE_V2_SUPPORTED_SOURCE_TRANSFORM_VERSIONS = Object.freeze([
20
+ LOCAL_MESSAGE_BUNDLE_V2_SOURCE_TRANSFORM_VERSION
21
+ ]);
22
+ var LOCAL_MESSAGE_BUNDLE_V2_LIMITS = Object.freeze({
23
+ manifestBytes: 1024 * 1024,
24
+ records: 500000,
25
+ recordBytes: 2 * 1024 * 1024,
26
+ totalBytes: 512 * 1024 * 1024,
27
+ accounts: 1,
28
+ identifierBytes: 1024,
29
+ shortTextBytes: 8 * 1024,
30
+ bodyBytes: 1024 * 1024,
31
+ mimeTypeBytes: 256,
32
+ participantsPerConversation: 1e4,
33
+ attachmentsPerMessage: 256,
34
+ warnings: 128
35
+ });
36
+ var LOCAL_MESSAGE_BUNDLE_V2_ARTIFACTS = Object.freeze([
37
+ Object.freeze({ path: "accounts.ndjson", kind: "account" }),
38
+ Object.freeze({ path: "participants.ndjson", kind: "participant" }),
39
+ Object.freeze({ path: "conversations.ndjson", kind: "conversation" }),
40
+ Object.freeze({ path: "messages.ndjson", kind: "message" }),
41
+ Object.freeze({ path: "reactions.ndjson", kind: "reaction" }),
42
+ Object.freeze({ path: "tombstones.ndjson", kind: "tombstone" })
43
+ ]);
44
+
45
+ class MessageBundleV2ContractError extends TypeError {
46
+ code = "message-bundle-v2-contract";
47
+ constructor(message, options) {
48
+ super(message, options);
49
+ this.name = "MessageBundleV2ContractError";
50
+ }
51
+ }
52
+ function fail(message) {
53
+ throw new MessageBundleV2ContractError(message);
54
+ }
55
+ function object(value, label) {
56
+ if (value === null || typeof value !== "object" || Array.isArray(value) || nodeTypes.isProxy(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
57
+ return fail(`${label} must be a plain object`);
58
+ for (const key of Reflect.ownKeys(value)) {
59
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
60
+ if (typeof key !== "string" || descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor))
61
+ return fail(`${label} must contain only enumerable string data properties`);
62
+ }
63
+ return value;
64
+ }
65
+ function exactKeys(value, keys, label) {
66
+ const expected = [...keys].sort();
67
+ const observed = Reflect.ownKeys(value).map(String).sort();
68
+ if (expected.length !== observed.length || observed.some((key, index) => key !== expected[index]))
69
+ fail(`${label} must contain exactly: ${keys.join(", ")}`);
70
+ }
71
+ function utf8Bytes(value) {
72
+ return new TextEncoder().encode(value).byteLength;
73
+ }
74
+ function boundedText(value, label, maximum) {
75
+ if (typeof value !== "string" || utf8Bytes(value) > maximum || value.includes("\x00")) {
76
+ return fail(`${label} must be NUL-free text within ${maximum} UTF-8 bytes`);
77
+ }
78
+ return value;
79
+ }
80
+ function nullableText(value, label, maximum) {
81
+ return value === null ? null : boundedText(value, label, maximum);
82
+ }
83
+ function identifier(value, label) {
84
+ const result = boundedText(value, label, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.identifierBytes);
85
+ if (result.length === 0 || /[\u0000-\u001f\u007f]/u.test(result)) {
86
+ return fail(`${label} must be a non-empty identifier without ASCII controls`);
87
+ }
88
+ return result;
89
+ }
90
+ function nullableIdentifier(value, label) {
91
+ return value === null ? null : identifier(value, label);
92
+ }
93
+ function parseLocalMessageBundleV2WhatsAppJid(value, label = "WhatsApp JID") {
94
+ const jid = identifier(value, label);
95
+ const user = /^([1-9][0-9]{4,14})@s\.whatsapp\.net$/u.exec(jid);
96
+ if (user !== null) {
97
+ return Object.freeze({ jid, kind: "user", e164: `+${user[1]}` });
98
+ }
99
+ if (/^[1-9][0-9]{4,19}@lid$/u.test(jid)) {
100
+ return Object.freeze({ jid, kind: "lid", e164: null });
101
+ }
102
+ if (/^[1-9][0-9]{4,19}(?:-[1-9][0-9]{0,19})?@g\.us$/u.test(jid)) {
103
+ return Object.freeze({ jid, kind: "group", e164: null });
104
+ }
105
+ return fail(`${label} must be a canonical user, LID, or group WhatsApp JID`);
106
+ }
107
+ function exactJidHandle(value, jid, label) {
108
+ const handle = nullableText(value, label, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.shortTextBytes);
109
+ if (jid.kind === "user" && handle !== jid.e164) {
110
+ return fail(`${label} must be the exact E.164 projection of its WhatsApp user JID`);
111
+ }
112
+ if (jid.kind !== "user" && handle !== null) {
113
+ return fail(`${label} must be null when the WhatsApp JID has no exact E.164 projection`);
114
+ }
115
+ return handle;
116
+ }
117
+ function token(value, label, maximum = 128) {
118
+ const result = boundedText(value, label, maximum);
119
+ if (!/^[a-z0-9](?:[a-z0-9._+-]*[a-z0-9])?$/u.test(result)) {
120
+ return fail(`${label} must be a lowercase categorical token`);
121
+ }
122
+ return result;
123
+ }
124
+ function version(value, label) {
125
+ const result = boundedText(value, label, 128);
126
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9._+-]*[A-Za-z0-9])?$/u.test(result)) {
127
+ return fail(`${label} must be a bounded version token`);
128
+ }
129
+ return result;
130
+ }
131
+ function oneOf(value, values, label) {
132
+ if (typeof value !== "string" || !values.includes(value)) {
133
+ return fail(`${label} must be one of: ${values.join(", ")}`);
134
+ }
135
+ return value;
136
+ }
137
+ function integer(value, label, maximum = Number.MAX_SAFE_INTEGER) {
138
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
139
+ return fail(`${label} must be a non-negative safe integer`);
140
+ }
141
+ return value;
142
+ }
143
+ function nullableInteger(value, label) {
144
+ return value === null ? null : integer(value, label);
145
+ }
146
+ function boolean(value, label) {
147
+ if (typeof value !== "boolean")
148
+ return fail(`${label} must be boolean`);
149
+ return value;
150
+ }
151
+ function nullableBoolean(value, label) {
152
+ return value === null ? null : boolean(value, label);
153
+ }
154
+ function timestamp(value, label) {
155
+ const result = boundedText(value, label, 64);
156
+ const date = new Date(result);
157
+ if (!Number.isFinite(date.getTime()) || date.toISOString() !== result) {
158
+ return fail(`${label} must be a canonical UTC timestamp`);
159
+ }
160
+ return result;
161
+ }
162
+ function nullableTimestamp(value, label) {
163
+ return value === null ? null : timestamp(value, label);
164
+ }
165
+ function digest(value, label) {
166
+ const result = boundedText(value, label, 64);
167
+ if (!/^[a-f0-9]{64}$/u.test(result))
168
+ return fail(`${label} must be lowercase SHA-256`);
169
+ return result;
170
+ }
171
+ function array(value, label, maximum) {
172
+ if (!Array.isArray(value) || nodeTypes.isProxy(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length > maximum) {
173
+ return fail(`${label} must contain at most ${maximum} items`);
174
+ }
175
+ const expectedKeys = new Set([
176
+ "length",
177
+ ...Array.from({ length: value.length }, (_item, index) => String(index))
178
+ ]);
179
+ for (const key of Reflect.ownKeys(value)) {
180
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
181
+ const isLength = key === "length";
182
+ if (typeof key !== "string" || !expectedKeys.has(key) || descriptor === undefined || !("value" in descriptor) || !isLength && descriptor.enumerable !== true)
183
+ return fail(`${label} must be a dense array of data properties`);
184
+ }
185
+ if (Reflect.ownKeys(value).length !== expectedKeys.size) {
186
+ return fail(`${label} must be a dense array of data properties`);
187
+ }
188
+ return value;
189
+ }
190
+ function identifiers(value, label, maximum) {
191
+ const result = array(value, label, maximum).map((item, index) => identifier(item, `${label}[${index}]`));
192
+ if (new Set(result).size !== result.length)
193
+ fail(`${label} repeats an ID`);
194
+ return Object.freeze(result);
195
+ }
196
+ function isLocalMessageBundleV2SourceTransformVersion(value) {
197
+ return value === LOCAL_MESSAGE_BUNDLE_V2_SOURCE_TRANSFORM_VERSION;
198
+ }
199
+ function assertLocalMessageBundleV2SourceTransformVersion(value, label = "manifest.source.version") {
200
+ const parsed = version(value, label);
201
+ if (!isLocalMessageBundleV2SourceTransformVersion(parsed)) {
202
+ return fail(`${label} must be a supported source transform: ${LOCAL_MESSAGE_BUNDLE_V2_SUPPORTED_SOURCE_TRANSFORM_VERSIONS.join(", ")}`);
203
+ }
204
+ return parsed;
205
+ }
206
+ function parseProvenance(value, label) {
207
+ const record = object(value, label);
208
+ exactKeys(record, [
209
+ "providerId",
210
+ "providerRevision",
211
+ "observedAt",
212
+ "connectedAccountProviderId"
213
+ ], label);
214
+ return Object.freeze({
215
+ providerId: identifier(record.providerId, `${label}.providerId`),
216
+ providerRevision: nullableIdentifier(record.providerRevision, `${label}.providerRevision`),
217
+ observedAt: timestamp(record.observedAt, `${label}.observedAt`),
218
+ connectedAccountProviderId: identifier(record.connectedAccountProviderId, `${label}.connectedAccountProviderId`)
219
+ });
220
+ }
221
+ function parseCommon(record, kind, extraKeys, label) {
222
+ exactKeys(record, [
223
+ "schemaVersion",
224
+ "kind",
225
+ "id",
226
+ "accountId",
227
+ "network",
228
+ "provenance",
229
+ ...extraKeys
230
+ ], label);
231
+ if (record.schemaVersion !== LOCAL_MESSAGE_BUNDLE_V2_SCHEMA_VERSION || record.kind !== kind) {
232
+ return fail(`${label} has the wrong schemaVersion or kind`);
233
+ }
234
+ if (record.network !== LOCAL_MESSAGE_BUNDLE_V2_NETWORK) {
235
+ return fail(`${label}.network must be ${LOCAL_MESSAGE_BUNDLE_V2_NETWORK}`);
236
+ }
237
+ const connectedAccount = parseLocalMessageBundleV2WhatsAppJid(object(record.provenance, `${label}.provenance`).connectedAccountProviderId, `${label}.provenance.connectedAccountProviderId`);
238
+ if (connectedAccount.kind === "group") {
239
+ return fail(`${label}.provenance.connectedAccountProviderId must be a WhatsApp user or LID JID`);
240
+ }
241
+ return Object.freeze({
242
+ schemaVersion: LOCAL_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
243
+ kind,
244
+ id: identifier(record.id, `${label}.id`),
245
+ accountId: identifier(record.accountId, `${label}.accountId`),
246
+ network: LOCAL_MESSAGE_BUNDLE_V2_NETWORK,
247
+ provenance: parseProvenance(record.provenance, `${label}.provenance`)
248
+ });
249
+ }
250
+ function parseAccount(record, label) {
251
+ const common = parseCommon(record, "account", [
252
+ "displayName",
253
+ "handle",
254
+ "selfParticipantId"
255
+ ], label);
256
+ if (common.id !== common.accountId || common.provenance.providerId !== common.provenance.connectedAccountProviderId)
257
+ return fail(`${label} does not establish one connected account realm`);
258
+ const providerJid = parseLocalMessageBundleV2WhatsAppJid(common.provenance.providerId, `${label}.provenance.providerId`);
259
+ if (providerJid.kind === "group") {
260
+ return fail(`${label}.provenance.providerId must be a WhatsApp user or LID JID`);
261
+ }
262
+ return Object.freeze({
263
+ ...common,
264
+ kind: "account",
265
+ displayName: nullableText(record.displayName, `${label}.displayName`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.shortTextBytes),
266
+ handle: exactJidHandle(record.handle, providerJid, `${label}.handle`),
267
+ selfParticipantId: identifier(record.selfParticipantId, `${label}.selfParticipantId`)
268
+ });
269
+ }
270
+ function parseParticipant(record, label) {
271
+ const common = parseCommon(record, "participant", ["displayName", "handle", "isSelf"], label);
272
+ const providerJid = parseLocalMessageBundleV2WhatsAppJid(common.provenance.providerId, `${label}.provenance.providerId`);
273
+ if (providerJid.kind === "group") {
274
+ return fail(`${label}.provenance.providerId must identify a WhatsApp user or LID participant`);
275
+ }
276
+ return Object.freeze({
277
+ ...common,
278
+ kind: "participant",
279
+ displayName: nullableText(record.displayName, `${label}.displayName`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.shortTextBytes),
280
+ handle: exactJidHandle(record.handle, providerJid, `${label}.handle`),
281
+ isSelf: boolean(record.isSelf, `${label}.isSelf`)
282
+ });
283
+ }
284
+ function parseConversation(record, label) {
285
+ const common = parseCommon(record, "conversation", [
286
+ "type",
287
+ "title",
288
+ "participantIds",
289
+ "participantsComplete",
290
+ "startedAt",
291
+ "lastMessageAt"
292
+ ], label);
293
+ const startedAt = nullableTimestamp(record.startedAt, `${label}.startedAt`);
294
+ const lastMessageAt = nullableTimestamp(record.lastMessageAt, `${label}.lastMessageAt`);
295
+ if (startedAt !== null && lastMessageAt !== null && startedAt > lastMessageAt) {
296
+ return fail(`${label}.startedAt must not follow lastMessageAt`);
297
+ }
298
+ const type = oneOf(record.type, ["direct", "group"], `${label}.type`);
299
+ const providerJid = parseLocalMessageBundleV2WhatsAppJid(common.provenance.providerId, `${label}.provenance.providerId`);
300
+ if (type === "direct" && providerJid.kind === "group" || type === "group" && providerJid.kind !== "group")
301
+ return fail(`${label} type conflicts with its WhatsApp JID`);
302
+ const participantIds = identifiers(record.participantIds, `${label}.participantIds`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.participantsPerConversation);
303
+ const participantsComplete = nullableBoolean(record.participantsComplete, `${label}.participantsComplete`);
304
+ if (type === "direct" && (participantsComplete !== true || participantIds.length !== 2)) {
305
+ return fail(`${label} direct roster must contain exactly two proven participants`);
306
+ }
307
+ return Object.freeze({
308
+ ...common,
309
+ kind: "conversation",
310
+ type,
311
+ title: nullableText(record.title, `${label}.title`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.shortTextBytes),
312
+ participantIds,
313
+ participantsComplete,
314
+ startedAt,
315
+ lastMessageAt
316
+ });
317
+ }
318
+ function parseReply(value, label) {
319
+ if (value === null)
320
+ return null;
321
+ const record = object(value, label);
322
+ exactKeys(record, ["messageId", "providerId"], label);
323
+ return Object.freeze({
324
+ messageId: nullableIdentifier(record.messageId, `${label}.messageId`),
325
+ providerId: identifier(record.providerId, `${label}.providerId`)
326
+ });
327
+ }
328
+ function parseEdit(value, sentAt, label) {
329
+ if (value === null)
330
+ return null;
331
+ const record = object(value, label);
332
+ if (record.kind === "in-place") {
333
+ exactKeys(record, ["kind", "editedAt", "providerRevision"], label);
334
+ const editedAt2 = timestamp(record.editedAt, `${label}.editedAt`);
335
+ if (editedAt2 < sentAt)
336
+ return fail(`${label} precedes the message`);
337
+ return Object.freeze({
338
+ kind: "in-place",
339
+ editedAt: editedAt2,
340
+ providerRevision: identifier(record.providerRevision, `${label}.providerRevision`)
341
+ });
342
+ }
343
+ if (record.kind !== "replacement") {
344
+ return fail(`${label}.kind must be in-place or replacement`);
345
+ }
346
+ exactKeys(record, [
347
+ "kind",
348
+ "replacesMessageId",
349
+ "replacesProviderId",
350
+ "editedAt",
351
+ "providerRevision"
352
+ ], label);
353
+ const editedAt = timestamp(record.editedAt, `${label}.editedAt`);
354
+ if (editedAt < sentAt)
355
+ return fail(`${label} precedes the message`);
356
+ return Object.freeze({
357
+ kind: "replacement",
358
+ replacesMessageId: nullableIdentifier(record.replacesMessageId, `${label}.replacesMessageId`),
359
+ replacesProviderId: identifier(record.replacesProviderId, `${label}.replacesProviderId`),
360
+ editedAt,
361
+ providerRevision: identifier(record.providerRevision, `${label}.providerRevision`)
362
+ });
363
+ }
364
+ function parseDeletion(value, label) {
365
+ if (value === null)
366
+ return null;
367
+ const record = object(value, label);
368
+ exactKeys(record, ["state", "observedAt", "providerRevision"], label);
369
+ return Object.freeze({
370
+ state: oneOf(record.state, ["revoked", "deleted-for-me", "revoked-and-deleted-for-me"], `${label}.state`),
371
+ observedAt: timestamp(record.observedAt, `${label}.observedAt`),
372
+ providerRevision: nullableIdentifier(record.providerRevision, `${label}.providerRevision`)
373
+ });
374
+ }
375
+ function parseAttachments(value, label) {
376
+ return Object.freeze(array(value, label, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.attachmentsPerMessage).map((item, index) => {
377
+ const itemLabel = `${label}[${index}]`;
378
+ const record = object(item, itemLabel);
379
+ exactKeys(record, ["kind", "mimeType", "name", "sizeBytes"], itemLabel);
380
+ const name = nullableText(record.name, `${itemLabel}.name`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.shortTextBytes);
381
+ if (name !== null && (name === "." || name === ".." || name.includes("/") || name.includes("\\"))) {
382
+ return fail(`${itemLabel}.name must not be a path`);
383
+ }
384
+ return Object.freeze({
385
+ kind: oneOf(record.kind, ["audio", "document", "image", "link", "sticker", "video", "unknown"], `${itemLabel}.kind`),
386
+ mimeType: nullableText(record.mimeType, `${itemLabel}.mimeType`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.mimeTypeBytes),
387
+ name,
388
+ sizeBytes: nullableInteger(record.sizeBytes, `${itemLabel}.sizeBytes`)
389
+ });
390
+ }));
391
+ }
392
+ function parseMessage(record, label) {
393
+ const common = parseCommon(record, "message", [
394
+ "conversationId",
395
+ "senderParticipantId",
396
+ "direction",
397
+ "sentAt",
398
+ "sortKey",
399
+ "body",
400
+ "bodyTruncated",
401
+ "replyTo",
402
+ "edit",
403
+ "deletion",
404
+ "attachments"
405
+ ], label);
406
+ const sentAt = timestamp(record.sentAt, `${label}.sentAt`);
407
+ const deletion = parseDeletion(record.deletion, `${label}.deletion`);
408
+ const body = nullableText(record.body, `${label}.body`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.bodyBytes);
409
+ if (deletion !== null && body !== null) {
410
+ return fail(`${label}.body must be null for a deleted message`);
411
+ }
412
+ return Object.freeze({
413
+ ...common,
414
+ kind: "message",
415
+ conversationId: identifier(record.conversationId, `${label}.conversationId`),
416
+ senderParticipantId: nullableIdentifier(record.senderParticipantId, `${label}.senderParticipantId`),
417
+ direction: oneOf(record.direction, ["incoming", "outgoing", "unknown"], `${label}.direction`),
418
+ sentAt,
419
+ sortKey: identifier(record.sortKey, `${label}.sortKey`),
420
+ body,
421
+ bodyTruncated: nullableBoolean(record.bodyTruncated, `${label}.bodyTruncated`),
422
+ replyTo: parseReply(record.replyTo, `${label}.replyTo`),
423
+ edit: parseEdit(record.edit, sentAt, `${label}.edit`),
424
+ deletion,
425
+ attachments: parseAttachments(record.attachments, `${label}.attachments`)
426
+ });
427
+ }
428
+ function parseReaction(record, label) {
429
+ const common = parseCommon(record, "reaction", [
430
+ "messageId",
431
+ "messageProviderId",
432
+ "participantId",
433
+ "body",
434
+ "reactedAt",
435
+ "state"
436
+ ], label);
437
+ return Object.freeze({
438
+ ...common,
439
+ kind: "reaction",
440
+ messageId: nullableIdentifier(record.messageId, `${label}.messageId`),
441
+ messageProviderId: identifier(record.messageProviderId, `${label}.messageProviderId`),
442
+ participantId: nullableIdentifier(record.participantId, `${label}.participantId`),
443
+ body: boundedText(record.body, `${label}.body`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.shortTextBytes),
444
+ reactedAt: nullableTimestamp(record.reactedAt, `${label}.reactedAt`),
445
+ state: oneOf(record.state, ["active", "removed"], `${label}.state`)
446
+ });
447
+ }
448
+ function parseTombstone(record, label) {
449
+ const common = parseCommon(record, "tombstone", [
450
+ "entityKind",
451
+ "entityId",
452
+ "entityProviderId",
453
+ "deletedAt",
454
+ "scope",
455
+ "providerRevision"
456
+ ], label);
457
+ return Object.freeze({
458
+ ...common,
459
+ kind: "tombstone",
460
+ entityKind: oneOf(record.entityKind, ["conversation", "message", "reaction"], `${label}.entityKind`),
461
+ entityId: nullableIdentifier(record.entityId, `${label}.entityId`),
462
+ entityProviderId: identifier(record.entityProviderId, `${label}.entityProviderId`),
463
+ deletedAt: timestamp(record.deletedAt, `${label}.deletedAt`),
464
+ scope: oneOf(record.scope, ["remote", "local", "unknown"], `${label}.scope`),
465
+ providerRevision: nullableIdentifier(record.providerRevision, `${label}.providerRevision`)
466
+ });
467
+ }
468
+ function parseLocalMessageBundleV2Record(value, kind, label = `${kind} record`) {
469
+ const record = object(value, label);
470
+ switch (kind) {
471
+ case "account":
472
+ return parseAccount(record, label);
473
+ case "participant":
474
+ return parseParticipant(record, label);
475
+ case "conversation":
476
+ return parseConversation(record, label);
477
+ case "message":
478
+ return parseMessage(record, label);
479
+ case "reaction":
480
+ return parseReaction(record, label);
481
+ case "tombstone":
482
+ return parseTombstone(record, label);
483
+ }
484
+ }
485
+ function parseArtifact(value, index) {
486
+ const expected = LOCAL_MESSAGE_BUNDLE_V2_ARTIFACTS[index];
487
+ const label = `manifest.artifacts[${index}]`;
488
+ const record = object(value, label);
489
+ exactKeys(record, ["path", "mediaType", "recordKind", "records", "bytes", "sha256"], label);
490
+ if (record.path !== expected.path || record.mediaType !== "application/x-ndjson" || record.recordKind !== expected.kind)
491
+ return fail(`${label} does not match the fixed artifact inventory`);
492
+ return Object.freeze({
493
+ path: expected.path,
494
+ mediaType: "application/x-ndjson",
495
+ recordKind: expected.kind,
496
+ records: integer(record.records, `${label}.records`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.records),
497
+ bytes: integer(record.bytes, `${label}.bytes`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.totalBytes),
498
+ sha256: digest(record.sha256, `${label}.sha256`)
499
+ });
500
+ }
501
+ function localMessageBundleV2ManifestProjection(manifest) {
502
+ const { integrity: _integrity, ...projection } = manifest;
503
+ return Object.freeze(projection);
504
+ }
505
+ function localMessageBundleV2BundleSha256(projection) {
506
+ return sha256(canonicalJson(projection));
507
+ }
508
+ function parseLocalMessageBundleV2Manifest(value) {
509
+ const record = object(value, "manifest");
510
+ exactKeys(record, [
511
+ "schemaVersion",
512
+ "format",
513
+ "source",
514
+ "provider",
515
+ "timestamps",
516
+ "completeness",
517
+ "warnings",
518
+ "privacy",
519
+ "counts",
520
+ "artifacts",
521
+ "integrity"
522
+ ], "manifest");
523
+ if (record.schemaVersion !== LOCAL_MESSAGE_BUNDLE_V2_SCHEMA_VERSION || record.format !== LOCAL_MESSAGE_BUNDLE_V2_FORMAT)
524
+ return fail("Manifest has an unsupported schemaVersion or format");
525
+ const source = object(record.source, "manifest.source");
526
+ exactKeys(source, ["id", "version"], "manifest.source");
527
+ if (source.id !== LOCAL_MESSAGE_BUNDLE_V2_SOURCE_ID) {
528
+ return fail(`manifest.source.id must be ${LOCAL_MESSAGE_BUNDLE_V2_SOURCE_ID}`);
529
+ }
530
+ const provider = object(record.provider, "manifest.provider");
531
+ exactKeys(provider, ["id", "version"], "manifest.provider");
532
+ if (provider.id !== LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_ID) {
533
+ return fail(`manifest.provider.id must be ${LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_ID}`);
534
+ }
535
+ const timestamps = object(record.timestamps, "manifest.timestamps");
536
+ exactKeys(timestamps, ["startedAt", "finishedAt", "createdAt"], "manifest.timestamps");
537
+ const startedAt = timestamp(timestamps.startedAt, "manifest.timestamps.startedAt");
538
+ const finishedAt = timestamp(timestamps.finishedAt, "manifest.timestamps.finishedAt");
539
+ const createdAt = timestamp(timestamps.createdAt, "manifest.timestamps.createdAt");
540
+ if (startedAt > finishedAt || finishedAt > createdAt) {
541
+ return fail("Manifest timestamps are not monotonic");
542
+ }
543
+ const completeness = object(record.completeness, "manifest.completeness");
544
+ exactKeys(completeness, [
545
+ "kind",
546
+ "reason",
547
+ "observedFrom",
548
+ "observedThrough"
549
+ ], "manifest.completeness");
550
+ const observedFrom = nullableTimestamp(completeness.observedFrom, "manifest.completeness.observedFrom");
551
+ const observedThrough = nullableTimestamp(completeness.observedThrough, "manifest.completeness.observedThrough");
552
+ if (observedFrom !== null && observedThrough !== null && observedFrom > observedThrough) {
553
+ return fail("Manifest completeness bounds are reversed");
554
+ }
555
+ const warnings = array(record.warnings, "manifest.warnings", LOCAL_MESSAGE_BUNDLE_V2_LIMITS.warnings).map((item, index) => token(item, `manifest.warnings[${index}]`));
556
+ if (new Set(warnings).size !== warnings.length)
557
+ fail("Manifest warnings repeat");
558
+ const privacy = object(record.privacy, "manifest.privacy");
559
+ exactKeys(privacy, [
560
+ "classification",
561
+ "attachments",
562
+ "providerUrls",
563
+ "credentials"
564
+ ], "manifest.privacy");
565
+ if (privacy.classification !== "private-local" || privacy.attachments !== "metadata-only" || privacy.providerUrls !== "excluded" || privacy.credentials !== "excluded")
566
+ return fail("Manifest privacy guarantees are unsupported");
567
+ const counts = object(record.counts, "manifest.counts");
568
+ exactKeys(counts, LOCAL_MESSAGE_BUNDLE_V2_ARTIFACTS.map(({ kind }) => kind), "manifest.counts");
569
+ const parsedCounts = Object.fromEntries(LOCAL_MESSAGE_BUNDLE_V2_ARTIFACTS.map(({ kind }) => [
570
+ kind,
571
+ integer(counts[kind], `manifest.counts.${kind}`, LOCAL_MESSAGE_BUNDLE_V2_LIMITS.records)
572
+ ]));
573
+ if (parsedCounts.account > LOCAL_MESSAGE_BUNDLE_V2_LIMITS.accounts) {
574
+ return fail(`Manifest exceeds the ${LOCAL_MESSAGE_BUNDLE_V2_LIMITS.accounts}-account safety bound`);
575
+ }
576
+ if (parsedCounts.account !== 1) {
577
+ return fail("A native Wacli bundle must contain exactly one connected account");
578
+ }
579
+ const artifactValues = array(record.artifacts, "manifest.artifacts", LOCAL_MESSAGE_BUNDLE_V2_ARTIFACTS.length);
580
+ if (artifactValues.length !== LOCAL_MESSAGE_BUNDLE_V2_ARTIFACTS.length) {
581
+ return fail("Manifest must list the fixed six artifacts");
582
+ }
583
+ const artifacts = Object.freeze(artifactValues.map(parseArtifact));
584
+ let totalRecords = 0;
585
+ let totalBytes = 0;
586
+ for (const artifact of artifacts) {
587
+ if (artifact.records !== parsedCounts[artifact.recordKind]) {
588
+ return fail(`${artifact.path} count disagrees with manifest.counts`);
589
+ }
590
+ totalRecords += artifact.records;
591
+ totalBytes += artifact.bytes;
592
+ }
593
+ if (totalRecords > LOCAL_MESSAGE_BUNDLE_V2_LIMITS.records || totalBytes > LOCAL_MESSAGE_BUNDLE_V2_LIMITS.totalBytes)
594
+ return fail("Manifest exceeds the bundle record or byte bound");
595
+ const integrity = object(record.integrity, "manifest.integrity");
596
+ exactKeys(integrity, ["algorithm", "bundleSha256"], "manifest.integrity");
597
+ if (integrity.algorithm !== "sha256") {
598
+ return fail("Manifest integrity algorithm is unsupported");
599
+ }
600
+ const result = Object.freeze({
601
+ schemaVersion: LOCAL_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
602
+ format: LOCAL_MESSAGE_BUNDLE_V2_FORMAT,
603
+ source: Object.freeze({
604
+ id: LOCAL_MESSAGE_BUNDLE_V2_SOURCE_ID,
605
+ version: assertLocalMessageBundleV2SourceTransformVersion(source.version)
606
+ }),
607
+ provider: Object.freeze({
608
+ id: LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_ID,
609
+ version: (() => {
610
+ const parsed = version(provider.version, "manifest.provider.version");
611
+ if (parsed !== LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_VERSION) {
612
+ return fail(`manifest.provider.version must be ${LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_VERSION}`);
613
+ }
614
+ return LOCAL_MESSAGE_BUNDLE_V2_PROVIDER_VERSION;
615
+ })()
616
+ }),
617
+ timestamps: Object.freeze({ startedAt, finishedAt, createdAt }),
618
+ completeness: Object.freeze({
619
+ kind: oneOf(completeness.kind, ["bounded-local", "truncated", "unknown"], "manifest.completeness.kind"),
620
+ reason: completeness.reason === null ? null : token(completeness.reason, "manifest.completeness.reason"),
621
+ observedFrom,
622
+ observedThrough
623
+ }),
624
+ warnings: Object.freeze(warnings),
625
+ privacy: Object.freeze({
626
+ classification: "private-local",
627
+ attachments: "metadata-only",
628
+ providerUrls: "excluded",
629
+ credentials: "excluded"
630
+ }),
631
+ counts: Object.freeze(parsedCounts),
632
+ artifacts,
633
+ integrity: Object.freeze({
634
+ algorithm: "sha256",
635
+ bundleSha256: digest(integrity.bundleSha256, "manifest.integrity.bundleSha256")
636
+ })
637
+ });
638
+ if (localMessageBundleV2BundleSha256(localMessageBundleV2ManifestProjection(result)) !== result.integrity.bundleSha256)
639
+ return fail("Manifest bundle SHA-256 does not match its canonical projection");
640
+ return result;
641
+ }
24
642
  export {
25
643
  parseLocalMessageBundleV2WhatsAppJid,
26
644
  parseLocalMessageBundleV2Record,