@tagea/capacitor-matrix 0.0.2 → 0.2.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.
@@ -15,27 +15,41 @@ class MatrixWeb extends core.WebPlugin {
15
15
  super(...arguments);
16
16
  this._cryptoCallbacks = {
17
17
  getSecretStorageKey: async (opts) => {
18
- var _a;
18
+ var _a, _b;
19
19
  const keyId = Object.keys(opts.keys)[0];
20
20
  if (!keyId)
21
21
  return null;
22
- // If we have the raw key cached, use it directly
23
- if (this.secretStorageKey) {
22
+ // Exact match: only return the cached raw key for the key ID it was cached under.
23
+ // (bootstrapSecretStorage uses createSecretStorageKey for the new key, so this
24
+ // path is only reached for an already-established key — e.g. after recoverAndSetup.)
25
+ if (this.secretStorageKey && this.secretStorageKeyId === keyId) {
24
26
  return [keyId, this.secretStorageKey];
25
27
  }
26
- // If we have a passphrase, derive the key using the server's stored parameters
28
+ // Derive from the current passphrase (set during recoverAndSetup)
27
29
  if (this.recoveryPassphrase) {
28
30
  const keyInfo = opts.keys[keyId];
29
31
  if (keyInfo === null || keyInfo === void 0 ? void 0 : keyInfo.passphrase) {
30
32
  const derived = await keyPassphrase.deriveRecoveryKeyFromPassphrase(this.recoveryPassphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations, (_a = keyInfo.passphrase.bits) !== null && _a !== void 0 ? _a : 256);
33
+ // Cache with the correct key ID for subsequent calls
31
34
  this.secretStorageKey = derived;
35
+ this.secretStorageKeyId = keyId;
36
+ return [keyId, derived];
37
+ }
38
+ }
39
+ // Fallback: derive from the OLD passphrase when bootstrapSecretStorage is
40
+ // migrating existing cross-signing / backup secrets into a new SSSS.
41
+ if (this.fallbackPassphrase) {
42
+ const keyInfo = opts.keys[keyId];
43
+ if (keyInfo === null || keyInfo === void 0 ? void 0 : keyInfo.passphrase) {
44
+ const derived = await keyPassphrase.deriveRecoveryKeyFromPassphrase(this.fallbackPassphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations, (_b = keyInfo.passphrase.bits) !== null && _b !== void 0 ? _b : 256);
32
45
  return [keyId, derived];
33
46
  }
34
47
  }
35
48
  return null;
36
49
  },
37
- cacheSecretStorageKey: (_keyId, _keyInfo, key) => {
50
+ cacheSecretStorageKey: (keyId, _keyInfo, key) => {
38
51
  this.secretStorageKey = key;
52
+ this.secretStorageKeyId = keyId;
39
53
  },
40
54
  };
41
55
  }
@@ -60,6 +74,12 @@ class MatrixWeb extends core.WebPlugin {
60
74
  return session;
61
75
  }
62
76
  async loginWithToken(options) {
77
+ // Stop any previously running client to avoid parallel instances that
78
+ // would deadlock on the shared IndexedDB crypto store.
79
+ if (this.client) {
80
+ this.client.stopClient();
81
+ this.client = undefined;
82
+ }
63
83
  this.client = matrixJsSdk.createClient({
64
84
  baseUrl: options.homeserverUrl,
65
85
  accessToken: options.accessToken,
@@ -89,6 +109,19 @@ class MatrixWeb extends core.WebPlugin {
89
109
  }
90
110
  localStorage.removeItem(SESSION_KEY);
91
111
  }
112
+ async clearAllData() {
113
+ if (this.client) {
114
+ this.client.stopClient();
115
+ this.client = undefined;
116
+ }
117
+ // Reset all cached crypto state
118
+ this.secretStorageKey = undefined;
119
+ this.secretStorageKeyId = undefined;
120
+ this.recoveryPassphrase = undefined;
121
+ this.fallbackPassphrase = undefined;
122
+ localStorage.removeItem(SESSION_KEY);
123
+ await this.deleteCryptoStore();
124
+ }
92
125
  async getSession() {
93
126
  const raw = localStorage.getItem(SESSION_KEY);
94
127
  if (!raw)
@@ -141,13 +174,21 @@ class MatrixWeb extends core.WebPlugin {
141
174
  }
142
175
  }
143
176
  });
144
- this.client.on(matrixJsSdk.RoomEvent.Receipt, (_event, room) => {
145
- var _a;
146
- this.notifyListeners('receiptReceived', {
147
- roomId: room.roomId,
148
- });
177
+ this.client.on(matrixJsSdk.RoomEvent.Receipt, (event, room) => {
178
+ var _a, _b;
179
+ const receiptContent = event.getContent();
180
+ for (const [eventId, receiptTypes] of Object.entries(receiptContent)) {
181
+ const mRead = (_a = receiptTypes['m.read']) !== null && _a !== void 0 ? _a : {};
182
+ for (const userId of Object.keys(mRead)) {
183
+ this.notifyListeners('receiptReceived', {
184
+ roomId: room.roomId,
185
+ eventId,
186
+ userId,
187
+ });
188
+ }
189
+ }
149
190
  // Re-emit own sent messages with updated read status
150
- const myUserId = (_a = this.client) === null || _a === void 0 ? void 0 : _a.getUserId();
191
+ const myUserId = (_b = this.client) === null || _b === void 0 ? void 0 : _b.getUserId();
151
192
  if (myUserId) {
152
193
  const timeline = room.getLiveTimeline().getEvents();
153
194
  // Walk backwards through recent events; stop after checking a reasonable batch
@@ -170,13 +211,14 @@ class MatrixWeb extends core.WebPlugin {
170
211
  });
171
212
  });
172
213
  this.client.on(matrixJsSdk.RoomMemberEvent.Typing, (_event, member) => {
173
- var _a, _b;
174
214
  const roomId = member === null || member === void 0 ? void 0 : member.roomId;
175
215
  if (roomId) {
176
216
  const room = this.client.getRoom(roomId);
177
217
  if (room) {
178
- const typingEvent = room.currentState.getStateEvents('m.typing', '');
179
- const userIds = (_b = (_a = typingEvent === null || typingEvent === void 0 ? void 0 : typingEvent.getContent()) === null || _a === void 0 ? void 0 : _a.user_ids) !== null && _b !== void 0 ? _b : [];
218
+ const userIds = room
219
+ .getMembers()
220
+ .filter((m) => m.typing)
221
+ .map((m) => m.userId);
180
222
  this.notifyListeners('typingChanged', { roomId, userIds });
181
223
  }
182
224
  }
@@ -295,10 +337,7 @@ class MatrixWeb extends core.WebPlugin {
295
337
  msgtype,
296
338
  body: options.body || options.fileName || 'file',
297
339
  url: mxcUrl,
298
- info: {
299
- mimetype: options.mimeType,
300
- size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size,
301
- },
340
+ info: Object.assign(Object.assign(Object.assign({ mimetype: options.mimeType, size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size }, (options.duration !== undefined && { duration: options.duration })), (options.width !== undefined && { w: options.width })), (options.height !== undefined && { h: options.height })),
302
341
  };
303
342
  const res = await this.client.sendMessage(options.roomId, content);
304
343
  return { eventId: res.event_id };
@@ -317,19 +356,35 @@ class MatrixWeb extends core.WebPlugin {
317
356
  return { eventId: res.event_id };
318
357
  }
319
358
  async editMessage(options) {
359
+ var _a, _b;
320
360
  this.requireClient();
321
- const content = {
322
- msgtype: matrixJsSdk.MsgType.Text,
323
- body: `* ${options.newBody}`,
324
- 'm.new_content': {
325
- msgtype: matrixJsSdk.MsgType.Text,
361
+ const msgtype = (_a = options.msgtype) !== null && _a !== void 0 ? _a : 'm.text';
362
+ const mediaTypes = ['m.image', 'm.audio', 'm.video', 'm.file'];
363
+ let newContent;
364
+ if (mediaTypes.includes(msgtype) && options.fileUri) {
365
+ const response = await fetch(options.fileUri);
366
+ const blob = await response.blob();
367
+ const uploadRes = await this.client.uploadContent(blob, {
368
+ name: options.fileName,
369
+ type: options.mimeType,
370
+ });
371
+ newContent = {
372
+ msgtype,
373
+ body: options.newBody || options.fileName || 'file',
374
+ url: uploadRes.content_uri,
375
+ info: Object.assign(Object.assign(Object.assign({ mimetype: options.mimeType, size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size }, (options.duration !== undefined && { duration: options.duration })), (options.width !== undefined && { w: options.width })), (options.height !== undefined && { h: options.height })),
376
+ };
377
+ }
378
+ else {
379
+ newContent = {
380
+ msgtype,
326
381
  body: options.newBody,
327
- },
328
- 'm.relates_to': {
382
+ };
383
+ }
384
+ const content = Object.assign(Object.assign({}, newContent), { body: `* ${options.newBody}`, 'm.new_content': newContent, 'm.relates_to': {
329
385
  rel_type: 'm.replace',
330
386
  event_id: options.eventId,
331
- },
332
- };
387
+ } });
333
388
  const res = await this.client.sendMessage(options.roomId, content);
334
389
  return { eventId: res.event_id };
335
390
  }
@@ -351,10 +406,7 @@ class MatrixWeb extends core.WebPlugin {
351
406
  msgtype,
352
407
  body: options.body || options.fileName || 'file',
353
408
  url: uploadRes.content_uri,
354
- info: {
355
- mimetype: options.mimeType,
356
- size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size,
357
- },
409
+ info: Object.assign(Object.assign(Object.assign({ mimetype: options.mimeType, size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size }, (options.duration !== undefined && { duration: options.duration })), (options.width !== undefined && { w: options.width })), (options.height !== undefined && { h: options.height })),
358
410
  'm.relates_to': {
359
411
  'm.in_reply_to': {
360
412
  event_id: options.replyToEventId,
@@ -572,18 +624,31 @@ class MatrixWeb extends core.WebPlugin {
572
624
  }
573
625
  // ── Device Management ──────────────────────────────────
574
626
  async getDevices() {
575
- var _a;
627
+ var _a, _b;
576
628
  this.requireClient();
577
629
  const res = await this.client.getDevices();
578
- const devices = ((_a = res.devices) !== null && _a !== void 0 ? _a : []).map((d) => {
579
- var _a, _b, _c;
580
- return ({
630
+ const crypto = this.client.getCrypto();
631
+ const myUserId = (_a = this.client.getUserId()) !== null && _a !== void 0 ? _a : '';
632
+ const devices = await Promise.all(((_b = res.devices) !== null && _b !== void 0 ? _b : []).map(async (d) => {
633
+ var _a, _b, _c, _d;
634
+ let isCrossSigningVerified;
635
+ if (crypto) {
636
+ try {
637
+ const status = await crypto.getDeviceVerificationStatus(myUserId, d.device_id);
638
+ isCrossSigningVerified = (_a = status === null || status === void 0 ? void 0 : status.crossSigningVerified) !== null && _a !== void 0 ? _a : false;
639
+ }
640
+ catch (_e) {
641
+ // ignore — crypto may not be ready
642
+ }
643
+ }
644
+ return {
581
645
  deviceId: d.device_id,
582
- displayName: (_a = d.display_name) !== null && _a !== void 0 ? _a : undefined,
583
- lastSeenTs: (_b = d.last_seen_ts) !== null && _b !== void 0 ? _b : undefined,
584
- lastSeenIp: (_c = d.last_seen_ip) !== null && _c !== void 0 ? _c : undefined,
585
- });
586
- });
646
+ displayName: (_b = d.display_name) !== null && _b !== void 0 ? _b : undefined,
647
+ lastSeenTs: (_c = d.last_seen_ts) !== null && _c !== void 0 ? _c : undefined,
648
+ lastSeenIp: (_d = d.last_seen_ip) !== null && _d !== void 0 ? _d : undefined,
649
+ isCrossSigningVerified,
650
+ };
651
+ }));
587
652
  return { devices };
588
653
  }
589
654
  async deleteDevice(options) {
@@ -606,12 +671,48 @@ class MatrixWeb extends core.WebPlugin {
606
671
  }
607
672
  // ── Encryption ──────────────────────────────────────────
608
673
  async initializeCrypto() {
674
+ var _a, _b;
609
675
  this.requireClient();
610
- const userId = this.client.getUserId();
611
- const deviceId = this.client.getDeviceId();
612
- await this.client.initRustCrypto({
613
- cryptoDatabasePrefix: `matrix-js-sdk/${userId}/${deviceId}`,
614
- });
676
+ const cryptoOpts = { cryptoDatabasePrefix: 'matrix-js-sdk' };
677
+ try {
678
+ await this.client.initRustCrypto(cryptoOpts);
679
+ }
680
+ catch (e) {
681
+ // After logout + re-login the server issues a new deviceId, but the
682
+ // shared IndexedDB crypto store still references the old one.
683
+ // Delete the stale store and retry so crypto initialises cleanly.
684
+ if ((_a = e === null || e === void 0 ? void 0 : e.message) === null || _a === void 0 ? void 0 : _a.includes("account in the store doesn't match")) {
685
+ await this.deleteCryptoStore();
686
+ await this.client.initRustCrypto(cryptoOpts);
687
+ }
688
+ else {
689
+ throw e;
690
+ }
691
+ }
692
+ // Flush the initial /keys/query request that initRustCrypto enqueues.
693
+ // Without this, any call to getIdentity (e.g. via getCrossSigningStatus)
694
+ // will spin-wait and emit periodic WARN logs until sync processes it.
695
+ const crypto = this.client.getCrypto();
696
+ if ((_b = crypto === null || crypto === void 0 ? void 0 : crypto.outgoingRequestsManager) === null || _b === void 0 ? void 0 : _b.doProcessOutgoingRequests) {
697
+ await crypto.outgoingRequestsManager.doProcessOutgoingRequests();
698
+ }
699
+ }
700
+ async deleteCryptoStore() {
701
+ if (typeof indexedDB === 'undefined')
702
+ return;
703
+ try {
704
+ const dbs = await indexedDB.databases();
705
+ await Promise.all(dbs
706
+ .filter((db) => { var _a; return (_a = db.name) === null || _a === void 0 ? void 0 : _a.startsWith('matrix-js-sdk'); })
707
+ .map((db) => new Promise((resolve) => {
708
+ const req = indexedDB.deleteDatabase(db.name);
709
+ req.onsuccess = () => resolve();
710
+ req.onerror = () => resolve();
711
+ })));
712
+ }
713
+ catch (_a) {
714
+ // indexedDB.databases() not available in all environments
715
+ }
615
716
  }
616
717
  async getEncryptionStatus() {
617
718
  this.requireClient();
@@ -694,12 +795,39 @@ class MatrixWeb extends core.WebPlugin {
694
795
  var _a;
695
796
  const crypto = await this.ensureCrypto();
696
797
  const keyInfo = await crypto.createRecoveryKeyFromPassphrase(options === null || options === void 0 ? void 0 : options.passphrase);
798
+ // Pre-cache the new key bytes. secretStorageKeyId will be set by
799
+ // cacheSecretStorageKey once bootstrapSecretStorage writes the new key
800
+ // into SSSS and the SDK calls back.
697
801
  this.secretStorageKey = keyInfo.privateKey;
698
- await crypto.bootstrapSecretStorage({
699
- createSecretStorageKey: async () => keyInfo,
700
- setupNewSecretStorage: true,
701
- setupNewKeyBackup: true,
702
- });
802
+ this.secretStorageKeyId = undefined;
803
+ // If the caller provides the same or old passphrase, keep it so
804
+ // getSecretStorageKey can derive the key for the SDK.
805
+ if (options === null || options === void 0 ? void 0 : options.passphrase) {
806
+ this.recoveryPassphrase = options.passphrase;
807
+ }
808
+ // If the caller knows the OLD passphrase, keep it as fallbackPassphrase so
809
+ // that getSecretStorageKey can decrypt the existing SSSS during
810
+ // bootstrapSecretStorage's migration of cross-signing / backup secrets.
811
+ if (options === null || options === void 0 ? void 0 : options.existingPassphrase) {
812
+ this.fallbackPassphrase = options.existingPassphrase;
813
+ }
814
+ try {
815
+ const bootstrapPromise = crypto.bootstrapSecretStorage({
816
+ createSecretStorageKey: async () => keyInfo,
817
+ setupNewSecretStorage: true,
818
+ setupNewKeyBackup: true,
819
+ });
820
+ // Guard against SDK hanging when it can't retrieve the old SSSS key
821
+ const timeoutPromise = new Promise((_, reject) => {
822
+ setTimeout(() => reject(new Error('bootstrapSecretStorage timed out — the old SSSS key could not be retrieved')), 30000);
823
+ });
824
+ await Promise.race([bootstrapPromise, timeoutPromise]);
825
+ }
826
+ finally {
827
+ // Always clear transient crypto state so it doesn't bleed into subsequent calls.
828
+ this.fallbackPassphrase = undefined;
829
+ this.recoveryPassphrase = undefined;
830
+ }
703
831
  return { recoveryKey: (_a = keyInfo.encodedPrivateKey) !== null && _a !== void 0 ? _a : '' };
704
832
  }
705
833
  async isRecoveryEnabled() {
@@ -728,8 +856,23 @@ class MatrixWeb extends core.WebPlugin {
728
856
  await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
729
857
  }
730
858
  catch (e) {
731
- // Clear stale key material so the next attempt starts fresh
859
+ const msg = e instanceof Error ? e.message : String(e);
860
+ if (msg.includes('decryption key does not match')) {
861
+ // The passphrase is correct (SSSS decrypted fine), but the backup key
862
+ // stored in SSSS doesn't match the server's current backup. This happens
863
+ // when another client re-created the backup without updating SSSS, or
864
+ // vice-versa. Auto-fix by creating a new backup that matches the SSSS key.
865
+ // recoveryPassphrase / secretStorageKey are still set, so the
866
+ // getSecretStorageKey callback can decrypt the existing SSSS.
867
+ await crypto.bootstrapSecretStorage({
868
+ setupNewKeyBackup: true,
869
+ });
870
+ await crypto.checkKeyBackupAndEnable();
871
+ return;
872
+ }
873
+ // Different error — clear state and throw
732
874
  this.secretStorageKey = undefined;
875
+ this.secretStorageKeyId = undefined;
733
876
  this.recoveryPassphrase = undefined;
734
877
  throw e;
735
878
  }