@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.
package/dist/esm/web.js CHANGED
@@ -8,27 +8,41 @@ export class MatrixWeb extends WebPlugin {
8
8
  super(...arguments);
9
9
  this._cryptoCallbacks = {
10
10
  getSecretStorageKey: async (opts) => {
11
- var _a;
11
+ var _a, _b;
12
12
  const keyId = Object.keys(opts.keys)[0];
13
13
  if (!keyId)
14
14
  return null;
15
- // If we have the raw key cached, use it directly
16
- if (this.secretStorageKey) {
15
+ // Exact match: only return the cached raw key for the key ID it was cached under.
16
+ // (bootstrapSecretStorage uses createSecretStorageKey for the new key, so this
17
+ // path is only reached for an already-established key — e.g. after recoverAndSetup.)
18
+ if (this.secretStorageKey && this.secretStorageKeyId === keyId) {
17
19
  return [keyId, this.secretStorageKey];
18
20
  }
19
- // If we have a passphrase, derive the key using the server's stored parameters
21
+ // Derive from the current passphrase (set during recoverAndSetup)
20
22
  if (this.recoveryPassphrase) {
21
23
  const keyInfo = opts.keys[keyId];
22
24
  if (keyInfo === null || keyInfo === void 0 ? void 0 : keyInfo.passphrase) {
23
25
  const derived = await deriveRecoveryKeyFromPassphrase(this.recoveryPassphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations, (_a = keyInfo.passphrase.bits) !== null && _a !== void 0 ? _a : 256);
26
+ // Cache with the correct key ID for subsequent calls
24
27
  this.secretStorageKey = derived;
28
+ this.secretStorageKeyId = keyId;
29
+ return [keyId, derived];
30
+ }
31
+ }
32
+ // Fallback: derive from the OLD passphrase when bootstrapSecretStorage is
33
+ // migrating existing cross-signing / backup secrets into a new SSSS.
34
+ if (this.fallbackPassphrase) {
35
+ const keyInfo = opts.keys[keyId];
36
+ if (keyInfo === null || keyInfo === void 0 ? void 0 : keyInfo.passphrase) {
37
+ const derived = await deriveRecoveryKeyFromPassphrase(this.fallbackPassphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations, (_b = keyInfo.passphrase.bits) !== null && _b !== void 0 ? _b : 256);
25
38
  return [keyId, derived];
26
39
  }
27
40
  }
28
41
  return null;
29
42
  },
30
- cacheSecretStorageKey: (_keyId, _keyInfo, key) => {
43
+ cacheSecretStorageKey: (keyId, _keyInfo, key) => {
31
44
  this.secretStorageKey = key;
45
+ this.secretStorageKeyId = keyId;
32
46
  },
33
47
  };
34
48
  }
@@ -53,6 +67,12 @@ export class MatrixWeb extends WebPlugin {
53
67
  return session;
54
68
  }
55
69
  async loginWithToken(options) {
70
+ // Stop any previously running client to avoid parallel instances that
71
+ // would deadlock on the shared IndexedDB crypto store.
72
+ if (this.client) {
73
+ this.client.stopClient();
74
+ this.client = undefined;
75
+ }
56
76
  this.client = createClient({
57
77
  baseUrl: options.homeserverUrl,
58
78
  accessToken: options.accessToken,
@@ -82,6 +102,19 @@ export class MatrixWeb extends WebPlugin {
82
102
  }
83
103
  localStorage.removeItem(SESSION_KEY);
84
104
  }
105
+ async clearAllData() {
106
+ if (this.client) {
107
+ this.client.stopClient();
108
+ this.client = undefined;
109
+ }
110
+ // Reset all cached crypto state
111
+ this.secretStorageKey = undefined;
112
+ this.secretStorageKeyId = undefined;
113
+ this.recoveryPassphrase = undefined;
114
+ this.fallbackPassphrase = undefined;
115
+ localStorage.removeItem(SESSION_KEY);
116
+ await this.deleteCryptoStore();
117
+ }
85
118
  async getSession() {
86
119
  const raw = localStorage.getItem(SESSION_KEY);
87
120
  if (!raw)
@@ -134,13 +167,21 @@ export class MatrixWeb extends WebPlugin {
134
167
  }
135
168
  }
136
169
  });
137
- this.client.on(RoomEvent.Receipt, (_event, room) => {
138
- var _a;
139
- this.notifyListeners('receiptReceived', {
140
- roomId: room.roomId,
141
- });
170
+ this.client.on(RoomEvent.Receipt, (event, room) => {
171
+ var _a, _b;
172
+ const receiptContent = event.getContent();
173
+ for (const [eventId, receiptTypes] of Object.entries(receiptContent)) {
174
+ const mRead = (_a = receiptTypes['m.read']) !== null && _a !== void 0 ? _a : {};
175
+ for (const userId of Object.keys(mRead)) {
176
+ this.notifyListeners('receiptReceived', {
177
+ roomId: room.roomId,
178
+ eventId,
179
+ userId,
180
+ });
181
+ }
182
+ }
142
183
  // Re-emit own sent messages with updated read status
143
- const myUserId = (_a = this.client) === null || _a === void 0 ? void 0 : _a.getUserId();
184
+ const myUserId = (_b = this.client) === null || _b === void 0 ? void 0 : _b.getUserId();
144
185
  if (myUserId) {
145
186
  const timeline = room.getLiveTimeline().getEvents();
146
187
  // Walk backwards through recent events; stop after checking a reasonable batch
@@ -288,10 +329,7 @@ export class MatrixWeb extends WebPlugin {
288
329
  msgtype,
289
330
  body: options.body || options.fileName || 'file',
290
331
  url: mxcUrl,
291
- info: {
292
- mimetype: options.mimeType,
293
- size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size,
294
- },
332
+ 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 })),
295
333
  };
296
334
  const res = await this.client.sendMessage(options.roomId, content);
297
335
  return { eventId: res.event_id };
@@ -310,19 +348,35 @@ export class MatrixWeb extends WebPlugin {
310
348
  return { eventId: res.event_id };
311
349
  }
312
350
  async editMessage(options) {
351
+ var _a, _b;
313
352
  this.requireClient();
314
- const content = {
315
- msgtype: MsgType.Text,
316
- body: `* ${options.newBody}`,
317
- 'm.new_content': {
318
- msgtype: MsgType.Text,
353
+ const msgtype = (_a = options.msgtype) !== null && _a !== void 0 ? _a : 'm.text';
354
+ const mediaTypes = ['m.image', 'm.audio', 'm.video', 'm.file'];
355
+ let newContent;
356
+ if (mediaTypes.includes(msgtype) && options.fileUri) {
357
+ const response = await fetch(options.fileUri);
358
+ const blob = await response.blob();
359
+ const uploadRes = await this.client.uploadContent(blob, {
360
+ name: options.fileName,
361
+ type: options.mimeType,
362
+ });
363
+ newContent = {
364
+ msgtype,
365
+ body: options.newBody || options.fileName || 'file',
366
+ url: uploadRes.content_uri,
367
+ 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 })),
368
+ };
369
+ }
370
+ else {
371
+ newContent = {
372
+ msgtype,
319
373
  body: options.newBody,
320
- },
321
- 'm.relates_to': {
374
+ };
375
+ }
376
+ const content = Object.assign(Object.assign({}, newContent), { body: `* ${options.newBody}`, 'm.new_content': newContent, 'm.relates_to': {
322
377
  rel_type: 'm.replace',
323
378
  event_id: options.eventId,
324
- },
325
- };
379
+ } });
326
380
  const res = await this.client.sendMessage(options.roomId, content);
327
381
  return { eventId: res.event_id };
328
382
  }
@@ -344,10 +398,7 @@ export class MatrixWeb extends WebPlugin {
344
398
  msgtype,
345
399
  body: options.body || options.fileName || 'file',
346
400
  url: uploadRes.content_uri,
347
- info: {
348
- mimetype: options.mimeType,
349
- size: (_b = options.fileSize) !== null && _b !== void 0 ? _b : blob.size,
350
- },
401
+ 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 })),
351
402
  'm.relates_to': {
352
403
  'm.in_reply_to': {
353
404
  event_id: options.replyToEventId,
@@ -565,18 +616,31 @@ export class MatrixWeb extends WebPlugin {
565
616
  }
566
617
  // ── Device Management ──────────────────────────────────
567
618
  async getDevices() {
568
- var _a;
619
+ var _a, _b;
569
620
  this.requireClient();
570
621
  const res = await this.client.getDevices();
571
- const devices = ((_a = res.devices) !== null && _a !== void 0 ? _a : []).map((d) => {
572
- var _a, _b, _c;
573
- return ({
622
+ const crypto = this.client.getCrypto();
623
+ const myUserId = (_a = this.client.getUserId()) !== null && _a !== void 0 ? _a : '';
624
+ const devices = await Promise.all(((_b = res.devices) !== null && _b !== void 0 ? _b : []).map(async (d) => {
625
+ var _a, _b, _c, _d;
626
+ let isCrossSigningVerified;
627
+ if (crypto) {
628
+ try {
629
+ const status = await crypto.getDeviceVerificationStatus(myUserId, d.device_id);
630
+ isCrossSigningVerified = (_a = status === null || status === void 0 ? void 0 : status.crossSigningVerified) !== null && _a !== void 0 ? _a : false;
631
+ }
632
+ catch (_e) {
633
+ // ignore — crypto may not be ready
634
+ }
635
+ }
636
+ return {
574
637
  deviceId: d.device_id,
575
- displayName: (_a = d.display_name) !== null && _a !== void 0 ? _a : undefined,
576
- lastSeenTs: (_b = d.last_seen_ts) !== null && _b !== void 0 ? _b : undefined,
577
- lastSeenIp: (_c = d.last_seen_ip) !== null && _c !== void 0 ? _c : undefined,
578
- });
579
- });
638
+ displayName: (_b = d.display_name) !== null && _b !== void 0 ? _b : undefined,
639
+ lastSeenTs: (_c = d.last_seen_ts) !== null && _c !== void 0 ? _c : undefined,
640
+ lastSeenIp: (_d = d.last_seen_ip) !== null && _d !== void 0 ? _d : undefined,
641
+ isCrossSigningVerified,
642
+ };
643
+ }));
580
644
  return { devices };
581
645
  }
582
646
  async deleteDevice(options) {
@@ -599,12 +663,48 @@ export class MatrixWeb extends WebPlugin {
599
663
  }
600
664
  // ── Encryption ──────────────────────────────────────────
601
665
  async initializeCrypto() {
666
+ var _a, _b;
602
667
  this.requireClient();
603
- const userId = this.client.getUserId();
604
- const deviceId = this.client.getDeviceId();
605
- await this.client.initRustCrypto({
606
- cryptoDatabasePrefix: `matrix-js-sdk/${userId}/${deviceId}`,
607
- });
668
+ const cryptoOpts = { cryptoDatabasePrefix: 'matrix-js-sdk' };
669
+ try {
670
+ await this.client.initRustCrypto(cryptoOpts);
671
+ }
672
+ catch (e) {
673
+ // After logout + re-login the server issues a new deviceId, but the
674
+ // shared IndexedDB crypto store still references the old one.
675
+ // Delete the stale store and retry so crypto initialises cleanly.
676
+ 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")) {
677
+ await this.deleteCryptoStore();
678
+ await this.client.initRustCrypto(cryptoOpts);
679
+ }
680
+ else {
681
+ throw e;
682
+ }
683
+ }
684
+ // Flush the initial /keys/query request that initRustCrypto enqueues.
685
+ // Without this, any call to getIdentity (e.g. via getCrossSigningStatus)
686
+ // will spin-wait and emit periodic WARN logs until sync processes it.
687
+ const crypto = this.client.getCrypto();
688
+ if ((_b = crypto === null || crypto === void 0 ? void 0 : crypto.outgoingRequestsManager) === null || _b === void 0 ? void 0 : _b.doProcessOutgoingRequests) {
689
+ await crypto.outgoingRequestsManager.doProcessOutgoingRequests();
690
+ }
691
+ }
692
+ async deleteCryptoStore() {
693
+ if (typeof indexedDB === 'undefined')
694
+ return;
695
+ try {
696
+ const dbs = await indexedDB.databases();
697
+ await Promise.all(dbs
698
+ .filter((db) => { var _a; return (_a = db.name) === null || _a === void 0 ? void 0 : _a.startsWith('matrix-js-sdk'); })
699
+ .map((db) => new Promise((resolve) => {
700
+ const req = indexedDB.deleteDatabase(db.name);
701
+ req.onsuccess = () => resolve();
702
+ req.onerror = () => resolve();
703
+ })));
704
+ }
705
+ catch (_a) {
706
+ // indexedDB.databases() not available in all environments
707
+ }
608
708
  }
609
709
  async getEncryptionStatus() {
610
710
  this.requireClient();
@@ -687,12 +787,39 @@ export class MatrixWeb extends WebPlugin {
687
787
  var _a;
688
788
  const crypto = await this.ensureCrypto();
689
789
  const keyInfo = await crypto.createRecoveryKeyFromPassphrase(options === null || options === void 0 ? void 0 : options.passphrase);
790
+ // Pre-cache the new key bytes. secretStorageKeyId will be set by
791
+ // cacheSecretStorageKey once bootstrapSecretStorage writes the new key
792
+ // into SSSS and the SDK calls back.
690
793
  this.secretStorageKey = keyInfo.privateKey;
691
- await crypto.bootstrapSecretStorage({
692
- createSecretStorageKey: async () => keyInfo,
693
- setupNewSecretStorage: true,
694
- setupNewKeyBackup: true,
695
- });
794
+ this.secretStorageKeyId = undefined;
795
+ // If the caller provides the same or old passphrase, keep it so
796
+ // getSecretStorageKey can derive the key for the SDK.
797
+ if (options === null || options === void 0 ? void 0 : options.passphrase) {
798
+ this.recoveryPassphrase = options.passphrase;
799
+ }
800
+ // If the caller knows the OLD passphrase, keep it as fallbackPassphrase so
801
+ // that getSecretStorageKey can decrypt the existing SSSS during
802
+ // bootstrapSecretStorage's migration of cross-signing / backup secrets.
803
+ if (options === null || options === void 0 ? void 0 : options.existingPassphrase) {
804
+ this.fallbackPassphrase = options.existingPassphrase;
805
+ }
806
+ try {
807
+ const bootstrapPromise = crypto.bootstrapSecretStorage({
808
+ createSecretStorageKey: async () => keyInfo,
809
+ setupNewSecretStorage: true,
810
+ setupNewKeyBackup: true,
811
+ });
812
+ // Guard against SDK hanging when it can't retrieve the old SSSS key
813
+ const timeoutPromise = new Promise((_, reject) => {
814
+ setTimeout(() => reject(new Error('bootstrapSecretStorage timed out — the old SSSS key could not be retrieved')), 30000);
815
+ });
816
+ await Promise.race([bootstrapPromise, timeoutPromise]);
817
+ }
818
+ finally {
819
+ // Always clear transient crypto state so it doesn't bleed into subsequent calls.
820
+ this.fallbackPassphrase = undefined;
821
+ this.recoveryPassphrase = undefined;
822
+ }
696
823
  return { recoveryKey: (_a = keyInfo.encodedPrivateKey) !== null && _a !== void 0 ? _a : '' };
697
824
  }
698
825
  async isRecoveryEnabled() {
@@ -721,8 +848,23 @@ export class MatrixWeb extends WebPlugin {
721
848
  await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
722
849
  }
723
850
  catch (e) {
724
- // Clear stale key material so the next attempt starts fresh
851
+ const msg = e instanceof Error ? e.message : String(e);
852
+ if (msg.includes('decryption key does not match')) {
853
+ // The passphrase is correct (SSSS decrypted fine), but the backup key
854
+ // stored in SSSS doesn't match the server's current backup. This happens
855
+ // when another client re-created the backup without updating SSSS, or
856
+ // vice-versa. Auto-fix by creating a new backup that matches the SSSS key.
857
+ // recoveryPassphrase / secretStorageKey are still set, so the
858
+ // getSecretStorageKey callback can decrypt the existing SSSS.
859
+ await crypto.bootstrapSecretStorage({
860
+ setupNewKeyBackup: true,
861
+ });
862
+ await crypto.checkKeyBackupAndEnable();
863
+ return;
864
+ }
865
+ // Different error — clear state and throw
725
866
  this.secretStorageKey = undefined;
867
+ this.secretStorageKeyId = undefined;
726
868
  this.recoveryPassphrase = undefined;
727
869
  throw e;
728
870
  }