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