@tagea/capacitor-matrix 0.0.2 → 0.1.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
@@ -295,10 +336,7 @@ class MatrixWeb extends core.WebPlugin {
295
336
  msgtype,
296
337
  body: options.body || options.fileName || 'file',
297
338
  url: mxcUrl,
298
- info: {
299
- mimetype: options.mimeType,
300
- size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size,
301
- },
339
+ 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
340
  };
303
341
  const res = await this.client.sendMessage(options.roomId, content);
304
342
  return { eventId: res.event_id };
@@ -317,19 +355,35 @@ class MatrixWeb extends core.WebPlugin {
317
355
  return { eventId: res.event_id };
318
356
  }
319
357
  async editMessage(options) {
358
+ var _a, _b;
320
359
  this.requireClient();
321
- const content = {
322
- msgtype: matrixJsSdk.MsgType.Text,
323
- body: `* ${options.newBody}`,
324
- 'm.new_content': {
325
- msgtype: matrixJsSdk.MsgType.Text,
360
+ const msgtype = (_a = options.msgtype) !== null && _a !== void 0 ? _a : 'm.text';
361
+ const mediaTypes = ['m.image', 'm.audio', 'm.video', 'm.file'];
362
+ let newContent;
363
+ if (mediaTypes.includes(msgtype) && options.fileUri) {
364
+ const response = await fetch(options.fileUri);
365
+ const blob = await response.blob();
366
+ const uploadRes = await this.client.uploadContent(blob, {
367
+ name: options.fileName,
368
+ type: options.mimeType,
369
+ });
370
+ newContent = {
371
+ msgtype,
372
+ body: options.newBody || options.fileName || 'file',
373
+ url: uploadRes.content_uri,
374
+ 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 })),
375
+ };
376
+ }
377
+ else {
378
+ newContent = {
379
+ msgtype,
326
380
  body: options.newBody,
327
- },
328
- 'm.relates_to': {
381
+ };
382
+ }
383
+ const content = Object.assign(Object.assign({}, newContent), { body: `* ${options.newBody}`, 'm.new_content': newContent, 'm.relates_to': {
329
384
  rel_type: 'm.replace',
330
385
  event_id: options.eventId,
331
- },
332
- };
386
+ } });
333
387
  const res = await this.client.sendMessage(options.roomId, content);
334
388
  return { eventId: res.event_id };
335
389
  }
@@ -351,10 +405,7 @@ class MatrixWeb extends core.WebPlugin {
351
405
  msgtype,
352
406
  body: options.body || options.fileName || 'file',
353
407
  url: uploadRes.content_uri,
354
- info: {
355
- mimetype: options.mimeType,
356
- size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size,
357
- },
408
+ 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
409
  'm.relates_to': {
359
410
  'm.in_reply_to': {
360
411
  event_id: options.replyToEventId,
@@ -572,18 +623,31 @@ class MatrixWeb extends core.WebPlugin {
572
623
  }
573
624
  // ── Device Management ──────────────────────────────────
574
625
  async getDevices() {
575
- var _a;
626
+ var _a, _b;
576
627
  this.requireClient();
577
628
  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 ({
629
+ const crypto = this.client.getCrypto();
630
+ const myUserId = (_a = this.client.getUserId()) !== null && _a !== void 0 ? _a : '';
631
+ const devices = await Promise.all(((_b = res.devices) !== null && _b !== void 0 ? _b : []).map(async (d) => {
632
+ var _a, _b, _c, _d;
633
+ let isCrossSigningVerified;
634
+ if (crypto) {
635
+ try {
636
+ const status = await crypto.getDeviceVerificationStatus(myUserId, d.device_id);
637
+ isCrossSigningVerified = (_a = status === null || status === void 0 ? void 0 : status.crossSigningVerified) !== null && _a !== void 0 ? _a : false;
638
+ }
639
+ catch (_e) {
640
+ // ignore — crypto may not be ready
641
+ }
642
+ }
643
+ return {
581
644
  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
- });
645
+ displayName: (_b = d.display_name) !== null && _b !== void 0 ? _b : undefined,
646
+ lastSeenTs: (_c = d.last_seen_ts) !== null && _c !== void 0 ? _c : undefined,
647
+ lastSeenIp: (_d = d.last_seen_ip) !== null && _d !== void 0 ? _d : undefined,
648
+ isCrossSigningVerified,
649
+ };
650
+ }));
587
651
  return { devices };
588
652
  }
589
653
  async deleteDevice(options) {
@@ -606,12 +670,48 @@ class MatrixWeb extends core.WebPlugin {
606
670
  }
607
671
  // ── Encryption ──────────────────────────────────────────
608
672
  async initializeCrypto() {
673
+ var _a, _b;
609
674
  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
- });
675
+ const cryptoOpts = { cryptoDatabasePrefix: 'matrix-js-sdk' };
676
+ try {
677
+ await this.client.initRustCrypto(cryptoOpts);
678
+ }
679
+ catch (e) {
680
+ // After logout + re-login the server issues a new deviceId, but the
681
+ // shared IndexedDB crypto store still references the old one.
682
+ // Delete the stale store and retry so crypto initialises cleanly.
683
+ 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")) {
684
+ await this.deleteCryptoStore();
685
+ await this.client.initRustCrypto(cryptoOpts);
686
+ }
687
+ else {
688
+ throw e;
689
+ }
690
+ }
691
+ // Flush the initial /keys/query request that initRustCrypto enqueues.
692
+ // Without this, any call to getIdentity (e.g. via getCrossSigningStatus)
693
+ // will spin-wait and emit periodic WARN logs until sync processes it.
694
+ const crypto = this.client.getCrypto();
695
+ if ((_b = crypto === null || crypto === void 0 ? void 0 : crypto.outgoingRequestsManager) === null || _b === void 0 ? void 0 : _b.doProcessOutgoingRequests) {
696
+ await crypto.outgoingRequestsManager.doProcessOutgoingRequests();
697
+ }
698
+ }
699
+ async deleteCryptoStore() {
700
+ if (typeof indexedDB === 'undefined')
701
+ return;
702
+ try {
703
+ const dbs = await indexedDB.databases();
704
+ await Promise.all(dbs
705
+ .filter((db) => { var _a; return (_a = db.name) === null || _a === void 0 ? void 0 : _a.startsWith('matrix-js-sdk'); })
706
+ .map((db) => new Promise((resolve) => {
707
+ const req = indexedDB.deleteDatabase(db.name);
708
+ req.onsuccess = () => resolve();
709
+ req.onerror = () => resolve();
710
+ })));
711
+ }
712
+ catch (_a) {
713
+ // indexedDB.databases() not available in all environments
714
+ }
615
715
  }
616
716
  async getEncryptionStatus() {
617
717
  this.requireClient();
@@ -694,12 +794,39 @@ class MatrixWeb extends core.WebPlugin {
694
794
  var _a;
695
795
  const crypto = await this.ensureCrypto();
696
796
  const keyInfo = await crypto.createRecoveryKeyFromPassphrase(options === null || options === void 0 ? void 0 : options.passphrase);
797
+ // Pre-cache the new key bytes. secretStorageKeyId will be set by
798
+ // cacheSecretStorageKey once bootstrapSecretStorage writes the new key
799
+ // into SSSS and the SDK calls back.
697
800
  this.secretStorageKey = keyInfo.privateKey;
698
- await crypto.bootstrapSecretStorage({
699
- createSecretStorageKey: async () => keyInfo,
700
- setupNewSecretStorage: true,
701
- setupNewKeyBackup: true,
702
- });
801
+ this.secretStorageKeyId = undefined;
802
+ // If the caller provides the same or old passphrase, keep it so
803
+ // getSecretStorageKey can derive the key for the SDK.
804
+ if (options === null || options === void 0 ? void 0 : options.passphrase) {
805
+ this.recoveryPassphrase = options.passphrase;
806
+ }
807
+ // If the caller knows the OLD passphrase, keep it as fallbackPassphrase so
808
+ // that getSecretStorageKey can decrypt the existing SSSS during
809
+ // bootstrapSecretStorage's migration of cross-signing / backup secrets.
810
+ if (options === null || options === void 0 ? void 0 : options.existingPassphrase) {
811
+ this.fallbackPassphrase = options.existingPassphrase;
812
+ }
813
+ try {
814
+ const bootstrapPromise = crypto.bootstrapSecretStorage({
815
+ createSecretStorageKey: async () => keyInfo,
816
+ setupNewSecretStorage: true,
817
+ setupNewKeyBackup: true,
818
+ });
819
+ // Guard against SDK hanging when it can't retrieve the old SSSS key
820
+ const timeoutPromise = new Promise((_, reject) => {
821
+ setTimeout(() => reject(new Error('bootstrapSecretStorage timed out — the old SSSS key could not be retrieved')), 30000);
822
+ });
823
+ await Promise.race([bootstrapPromise, timeoutPromise]);
824
+ }
825
+ finally {
826
+ // Always clear transient crypto state so it doesn't bleed into subsequent calls.
827
+ this.fallbackPassphrase = undefined;
828
+ this.recoveryPassphrase = undefined;
829
+ }
703
830
  return { recoveryKey: (_a = keyInfo.encodedPrivateKey) !== null && _a !== void 0 ? _a : '' };
704
831
  }
705
832
  async isRecoveryEnabled() {
@@ -728,8 +855,23 @@ class MatrixWeb extends core.WebPlugin {
728
855
  await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
729
856
  }
730
857
  catch (e) {
731
- // Clear stale key material so the next attempt starts fresh
858
+ const msg = e instanceof Error ? e.message : String(e);
859
+ if (msg.includes('decryption key does not match')) {
860
+ // The passphrase is correct (SSSS decrypted fine), but the backup key
861
+ // stored in SSSS doesn't match the server's current backup. This happens
862
+ // when another client re-created the backup without updating SSSS, or
863
+ // vice-versa. Auto-fix by creating a new backup that matches the SSSS key.
864
+ // recoveryPassphrase / secretStorageKey are still set, so the
865
+ // getSecretStorageKey callback can decrypt the existing SSSS.
866
+ await crypto.bootstrapSecretStorage({
867
+ setupNewKeyBackup: true,
868
+ });
869
+ await crypto.checkKeyBackupAndEnable();
870
+ return;
871
+ }
872
+ // Different error — clear state and throw
732
873
  this.secretStorageKey = undefined;
874
+ this.secretStorageKeyId = undefined;
733
875
  this.recoveryPassphrase = undefined;
734
876
  throw e;
735
877
  }