openclaw-app 1.0.7 → 1.0.8

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/index.ts CHANGED
@@ -241,6 +241,9 @@ interface RelayState {
241
241
  * so multiple users can be active simultaneously without key collisions.
242
242
  */
243
243
  e2eSessions: Map<string, E2EState>;
244
+ /** Previous E2E states kept for decrypting offline-buffered messages
245
+ * that were encrypted with the old key before the app reconnected. */
246
+ prevE2eSessions: Map<string, E2EState>;
244
247
  relayToken: string;
245
248
  }
246
249
 
@@ -261,6 +264,7 @@ function getRelayState(accountId: string): RelayState {
261
264
  statusSink: null,
262
265
  gatewayCtx: null,
263
266
  e2eSessions: new Map(),
267
+ prevE2eSessions: new Map(),
264
268
  relayToken: "",
265
269
  };
266
270
  relayStates.set(accountId, state);
@@ -555,8 +559,8 @@ function cleanupRelay(state: RelayState) {
555
559
  try { state.ws.close(); } catch {}
556
560
  state.ws = null;
557
561
  }
558
- // Clear all per-session E2E states — new handshakes needed on reconnect
559
562
  state.e2eSessions.clear();
563
+ state.prevE2eSessions.clear();
560
564
  }
561
565
 
562
566
  function connectRelay(ctx: any, account: ResolvedAccount) {
@@ -663,13 +667,14 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
663
667
  ctx.log?.warn?.(`[${CHANNEL_ID}] [${accountId}] [E2E] peer_joined missing sessionKey, ignoring`);
664
668
  return;
665
669
  }
666
- // Always restart E2E when peer_joined arrives the app may have
667
- // reconnected with a fresh _e2eReadyCompleter and is waiting for a
668
- // new handshake even if we still hold an old session state.
669
- if (state.e2eSessions.has(sessionKey)) {
670
- ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] Session ${sessionKey} reconnected, resetting E2E state`);
671
- state.e2eSessions.delete(sessionKey);
670
+ // Preserve old E2E state for decrypting offline-buffered messages,
671
+ // then create a fresh state for the new handshake.
672
+ const oldE2E = state.e2eSessions.get(sessionKey);
673
+ if (oldE2E?.ready) {
674
+ state.prevE2eSessions.set(sessionKey, oldE2E);
675
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] Session ${sessionKey} reconnected, old key preserved for pending_flush`);
672
676
  }
677
+ state.e2eSessions.delete(sessionKey);
673
678
  ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] App session joined (${sessionKey}), sending handshake`);
674
679
  const sessionE2E = makeE2EState();
675
680
  state.e2eSessions.set(sessionKey, sessionE2E);
@@ -700,6 +705,49 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
700
705
  return;
701
706
  }
702
707
 
708
+ // Relay forwards buffered offline messages for re-encryption.
709
+ // Each message was encrypted with the old E2E key; we decrypt with the
710
+ // preserved old key and re-encrypt with the current (new) session key.
711
+ if (msg.type === "pending_flush") {
712
+ const sessionKey = msg.sessionKey as string | undefined;
713
+ const messages = msg.messages as string[] | undefined;
714
+ if (!sessionKey || !messages || messages.length === 0) {
715
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: nothing to flush`);
716
+ return;
717
+ }
718
+ const oldE2E = state.prevE2eSessions.get(sessionKey);
719
+ const newE2E = state.e2eSessions.get(sessionKey);
720
+ if (!oldE2E?.ready) {
721
+ ctx.log?.warn?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: no old key for session ${sessionKey}, dropping ${messages.length} message(s)`);
722
+ return;
723
+ }
724
+ if (!newE2E?.ready) {
725
+ ctx.log?.warn?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: new E2E not ready for session ${sessionKey}, dropping`);
726
+ return;
727
+ }
728
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: re-encrypting ${messages.length} message(s) for session ${sessionKey}`);
729
+ for (const raw of messages) {
730
+ try {
731
+ const parsed = JSON.parse(raw);
732
+ if (parsed.type !== "encrypted" || !parsed.nonce || !parsed.ct) {
733
+ // Not encrypted — forward as-is with sessionKey
734
+ if (!parsed.sessionKey) parsed.sessionKey = sessionKey;
735
+ state.ws?.send(JSON.stringify(parsed));
736
+ continue;
737
+ }
738
+ const plaintext = await e2eDecrypt(oldE2E, parsed.nonce, parsed.ct);
739
+ const reEncrypted = JSON.parse(await e2eEncrypt(newE2E, plaintext));
740
+ reEncrypted.sessionKey = sessionKey;
741
+ state.ws?.send(JSON.stringify(reEncrypted));
742
+ } catch (e) {
743
+ ctx.log?.warn?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: failed to re-encrypt: ${e}`);
744
+ }
745
+ }
746
+ // Old key no longer needed after flush
747
+ state.prevE2eSessions.delete(sessionKey);
748
+ return;
749
+ }
750
+
703
751
  // App의 handshake 응답 수신 — ECDH 완성
704
752
  if (msg.type === "handshake") {
705
753
  const sessionKey = msg.sessionKey as string | undefined;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-app",
3
3
  "name": "OpenClaw App",
4
- "version": "1.0.7",
4
+ "version": "1.0.8",
5
5
  "description": "Mobile app channel for OpenClaw — chat via the OpenClaw Mobile app through a Cloudflare Worker relay.",
6
6
  "channels": [
7
7
  "openclaw-app"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-app",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "OpenClaw Mobile channel plugin — relay bridge for the OpenClaw Mobile app",
5
5
  "main": "index.ts",
6
6
  "type": "module",