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 +1 -1
- package/index.ts +75 -12
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
367
|
-
selectionLabel: "OpenClaw
|
|
368
|
-
blurb: "Chat via the OpenClaw
|
|
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
|
-
//
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
|
|
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
|
|
945
|
+
channel: "OpenClaw App",
|
|
883
946
|
from: `${senderName} (mobile)`,
|
|
884
947
|
body: text,
|
|
885
948
|
chatType: "direct",
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-app",
|
|
3
3
|
"name": "OpenClaw App",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"description": "Mobile app channel for OpenClaw — chat via the OpenClaw
|
|
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.
|
|
4
|
-
"description": "OpenClaw
|
|
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": {
|