openclaw-app 1.0.8 → 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 +52 -37
- 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.
|
|
@@ -244,6 +244,8 @@ interface RelayState {
|
|
|
244
244
|
/** Previous E2E states kept for decrypting offline-buffered messages
|
|
245
245
|
* that were encrypted with the old key before the app reconnected. */
|
|
246
246
|
prevE2eSessions: Map<string, E2EState>;
|
|
247
|
+
/** Buffered pending_flush payloads waiting for the new E2E handshake to complete. */
|
|
248
|
+
pendingFlushQueue: Map<string, string[]>;
|
|
247
249
|
relayToken: string;
|
|
248
250
|
}
|
|
249
251
|
|
|
@@ -265,6 +267,7 @@ function getRelayState(accountId: string): RelayState {
|
|
|
265
267
|
gatewayCtx: null,
|
|
266
268
|
e2eSessions: new Map(),
|
|
267
269
|
prevE2eSessions: new Map(),
|
|
270
|
+
pendingFlushQueue: new Map(),
|
|
268
271
|
relayToken: "",
|
|
269
272
|
};
|
|
270
273
|
relayStates.set(accountId, state);
|
|
@@ -367,9 +370,9 @@ const channel = {
|
|
|
367
370
|
id: CHANNEL_ID,
|
|
368
371
|
meta: {
|
|
369
372
|
id: CHANNEL_ID,
|
|
370
|
-
label: "OpenClaw
|
|
371
|
-
selectionLabel: "OpenClaw
|
|
372
|
-
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.",
|
|
373
376
|
detailLabel: "Mobile App",
|
|
374
377
|
aliases: ["mobile"],
|
|
375
378
|
},
|
|
@@ -561,6 +564,7 @@ function cleanupRelay(state: RelayState) {
|
|
|
561
564
|
}
|
|
562
565
|
state.e2eSessions.clear();
|
|
563
566
|
state.prevE2eSessions.clear();
|
|
567
|
+
state.pendingFlushQueue.clear();
|
|
564
568
|
}
|
|
565
569
|
|
|
566
570
|
function connectRelay(ctx: any, account: ResolvedAccount) {
|
|
@@ -650,6 +654,41 @@ function scheduleReconnect(ctx: any, account: ResolvedAccount) {
|
|
|
650
654
|
}, RECONNECT_DELAY);
|
|
651
655
|
}
|
|
652
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
|
+
|
|
653
692
|
async function handleRelayMessage(ctx: any, accountId: string, state: RelayState, raw: string): Promise<void> {
|
|
654
693
|
// Skip ping/pong
|
|
655
694
|
if (raw === "ping" || raw === "pong") return;
|
|
@@ -706,8 +745,6 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
|
|
|
706
745
|
}
|
|
707
746
|
|
|
708
747
|
// 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
748
|
if (msg.type === "pending_flush") {
|
|
712
749
|
const sessionKey = msg.sessionKey as string | undefined;
|
|
713
750
|
const messages = msg.messages as string[] | undefined;
|
|
@@ -715,36 +752,7 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
|
|
|
715
752
|
ctx.log?.info?.(`[${CHANNEL_ID}] [${accountId}] [E2E] pending_flush: nothing to flush`);
|
|
716
753
|
return;
|
|
717
754
|
}
|
|
718
|
-
|
|
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);
|
|
755
|
+
await processPendingFlush(ctx, accountId, state, sessionKey, messages);
|
|
748
756
|
return;
|
|
749
757
|
}
|
|
750
758
|
|
|
@@ -828,6 +836,13 @@ async function handleRelayMessage(ctx: any, accountId: string, state: RelayState
|
|
|
828
836
|
}
|
|
829
837
|
await e2eHandleHandshake(sessionE2E, peerPubKey);
|
|
830
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
|
+
}
|
|
831
846
|
return;
|
|
832
847
|
}
|
|
833
848
|
|
|
@@ -927,7 +942,7 @@ async function handleInbound(ctx: any, accountId: string, msg: any) {
|
|
|
927
942
|
);
|
|
928
943
|
|
|
929
944
|
const body = runtime.channel.reply.formatInboundEnvelope({
|
|
930
|
-
channel: "OpenClaw
|
|
945
|
+
channel: "OpenClaw App",
|
|
931
946
|
from: `${senderName} (mobile)`,
|
|
932
947
|
body: text,
|
|
933
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": {
|