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