openclaw-app 1.0.7 → 1.0.9

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/README.md CHANGED
@@ -90,7 +90,7 @@ openclaw gateway --port 18789
90
90
 
91
91
  ### 5. Verify
92
92
 
93
- Open the Control UI at http://127.0.0.1:18789/ and navigate to the **OpenClaw Mobile** channel page. You should see:
93
+ Open the Control UI at http://127.0.0.1:18789/ and navigate to the **OpenClaw App** channel page. You should see:
94
94
 
95
95
  - **Running**: Yes
96
96
  - **Configured**: Yes
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OpenClaw Mobile Channel Plugin
2
+ * OpenClaw App Channel Plugin
3
3
  *
4
4
  * Registers "openclaw-app" channel that bridges Gateway <-> CF Worker relay <-> Mobile App.
5
5
  * Plugin runs inside the Gateway process, connects outbound to the relay.
@@ -241,6 +241,11 @@ 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>;
247
+ /** Buffered pending_flush payloads waiting for the new E2E handshake to complete. */
248
+ pendingFlushQueue: Map<string, string[]>;
244
249
  relayToken: string;
245
250
  }
246
251
 
@@ -261,6 +266,8 @@ function getRelayState(accountId: string): RelayState {
261
266
  statusSink: null,
262
267
  gatewayCtx: null,
263
268
  e2eSessions: new Map(),
269
+ prevE2eSessions: new Map(),
270
+ pendingFlushQueue: new Map(),
264
271
  relayToken: "",
265
272
  };
266
273
  relayStates.set(accountId, state);
@@ -363,9 +370,9 @@ const channel = {
363
370
  id: CHANNEL_ID,
364
371
  meta: {
365
372
  id: CHANNEL_ID,
366
- label: "OpenClaw Mobile",
367
- selectionLabel: "OpenClaw Mobile App",
368
- blurb: "Chat via the OpenClaw Mobile app through a relay.",
373
+ label: "OpenClaw App",
374
+ selectionLabel: "OpenClaw App App",
375
+ blurb: "Chat via the OpenClaw App app through a relay.",
369
376
  detailLabel: "Mobile App",
370
377
  aliases: ["mobile"],
371
378
  },
@@ -555,8 +562,9 @@ function cleanupRelay(state: RelayState) {
555
562
  try { state.ws.close(); } catch {}
556
563
  state.ws = null;
557
564
  }
558
- // Clear all per-session E2E states — new handshakes needed on reconnect
559
565
  state.e2eSessions.clear();
566
+ state.prevE2eSessions.clear();
567
+ state.pendingFlushQueue.clear();
560
568
  }
561
569
 
562
570
  function connectRelay(ctx: any, account: ResolvedAccount) {
@@ -646,6 +654,41 @@ function scheduleReconnect(ctx: any, account: ResolvedAccount) {
646
654
  }, RECONNECT_DELAY);
647
655
  }
648
656
 
657
+ async function processPendingFlush(
658
+ ctx: any, accountId: string, state: RelayState, sessionKey: string, messages: string[]
659
+ ): Promise<void> {
660
+ const oldE2E = state.prevE2eSessions.get(sessionKey);
661
+ const newE2E = state.e2eSessions.get(sessionKey);
662
+ if (!oldE2E?.ready) {
663
+ ctx.log?.warn?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: no old key for session ${sessionKey}, dropping ${messages.length} message(s)`);
664
+ return;
665
+ }
666
+ if (!newE2E?.ready) {
667
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: new E2E not ready, queueing for session ${sessionKey}`);
668
+ state.pendingFlushQueue.set(sessionKey, messages);
669
+ return;
670
+ }
671
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: re-encrypting ${messages.length} message(s) for session ${sessionKey}`);
672
+ for (const raw of messages) {
673
+ try {
674
+ const parsed = JSON.parse(raw);
675
+ if (parsed.type !== "encrypted" || !parsed.nonce || !parsed.ct) {
676
+ if (!parsed.sessionKey) parsed.sessionKey = sessionKey;
677
+ state.ws?.send(JSON.stringify(parsed));
678
+ continue;
679
+ }
680
+ const plaintext = await e2eDecrypt(oldE2E, parsed.nonce, parsed.ct);
681
+ const reEncrypted = JSON.parse(await e2eEncrypt(newE2E, plaintext));
682
+ reEncrypted.sessionKey = sessionKey;
683
+ state.ws?.send(JSON.stringify(reEncrypted));
684
+ } catch (e) {
685
+ ctx.log?.warn?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: failed to re-encrypt: ${e}`);
686
+ }
687
+ }
688
+ state.prevE2eSessions.delete(sessionKey);
689
+ state.pendingFlushQueue.delete(sessionKey);
690
+ }
691
+
649
692
  async function handleRelayMessage(ctx: any, accountId: string, state: RelayState, raw: string): Promise<void> {
650
693
  // Skip ping/pong
651
694
  if (raw === "ping" || raw === "pong") return;
@@ -663,13 +706,14 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
663
706
  ctx.log?.warn?.(`[${CHANNEL_ID}] [${accountId}] [E2E] peer_joined missing sessionKey, ignoring`);
664
707
  return;
665
708
  }
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);
709
+ // Preserve old E2E state for decrypting offline-buffered messages,
710
+ // then create a fresh state for the new handshake.
711
+ const oldE2E = state.e2eSessions.get(sessionKey);
712
+ if (oldE2E?.ready) {
713
+ state.prevE2eSessions.set(sessionKey, oldE2E);
714
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] Session ${sessionKey} reconnected, old key preserved for pending_flush`);
672
715
  }
716
+ state.e2eSessions.delete(sessionKey);
673
717
  ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] App session joined (${sessionKey}), sending handshake`);
674
718
  const sessionE2E = makeE2EState();
675
719
  state.e2eSessions.set(sessionKey, sessionE2E);
@@ -700,6 +744,18 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
700
744
  return;
701
745
  }
702
746
 
747
+ // Relay forwards buffered offline messages for re-encryption.
748
+ if (msg.type === "pending_flush") {
749
+ const sessionKey = msg.sessionKey as string | undefined;
750
+ const messages = msg.messages as string[] | undefined;
751
+ if (!sessionKey || !messages || messages.length === 0) {
752
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: nothing to flush`);
753
+ return;
754
+ }
755
+ await processPendingFlush(ctx, accountId, state, sessionKey, messages);
756
+ return;
757
+ }
758
+
703
759
  // App의 handshake 응답 수신 — ECDH 완성
704
760
  if (msg.type === "handshake") {
705
761
  const sessionKey = msg.sessionKey as string | undefined;
@@ -780,6 +836,13 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
780
836
  }
781
837
  await e2eHandleHandshake(sessionE2E, peerPubKey);
782
838
  ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] Session ${sessionKey} handshake complete`);
839
+
840
+ // Process any queued pending_flush that arrived before this handshake completed
841
+ const queued = state.pendingFlushQueue.get(sessionKey);
842
+ if (queued && queued.length > 0) {
843
+ ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] Processing queued pending_flush for session ${sessionKey}`);
844
+ await processPendingFlush(ctx, accountId, state, sessionKey, queued);
845
+ }
783
846
  return;
784
847
  }
785
848
 
@@ -879,7 +942,7 @@ async function handleInbound(ctx: any, accountId: string, msg: any) {
879
942
  );
880
943
 
881
944
  const body = runtime.channel.reply.formatInboundEnvelope({
882
- channel: "OpenClaw Mobile",
945
+ channel: "OpenClaw App",
883
946
  from: `${senderName} (mobile)`,
884
947
  body: text,
885
948
  chatType: "direct",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "openclaw-app",
3
3
  "name": "OpenClaw App",
4
- "version": "1.0.7",
5
- "description": "Mobile app channel for OpenClaw — chat via the OpenClaw Mobile app through a Cloudflare Worker relay.",
4
+ "version": "1.0.9",
5
+ "description": "Mobile app channel for OpenClaw — chat via the OpenClaw App app through a Cloudflare Worker relay.",
6
6
  "channels": [
7
7
  "openclaw-app"
8
8
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openclaw-app",
3
- "version": "1.0.7",
4
- "description": "OpenClaw Mobile channel plugin — relay bridge for the OpenClaw Mobile app",
3
+ "version": "1.0.9",
4
+ "description": "OpenClaw App channel plugin — relay bridge for the OpenClaw App app",
5
5
  "main": "index.ts",
6
6
  "type": "module",
7
7
  "openclaw": {