@soyeht/soyeht 0.2.9 → 0.2.11
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 +72 -29
- package/docs/PROTOCOL.md +79 -15
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/http.ts +107 -1
- package/src/index.ts +6 -0
- package/src/outbound-queue.ts +13 -0
- package/src/pairing.ts +1 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Channel plugin for connecting the Soyeht Flutter mobile app to an OpenClaw gateway.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## V1 Architecture
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The app and the OpenClaw instance communicate over **HTTP + SSE** on a shared network (typically Tailscale).
|
|
8
|
+
|
|
9
|
+
- **Inbound** (app → plugin): `POST {gatewayUrl}/soyeht/messages/inbound`
|
|
10
|
+
- **Outbound** (plugin → app): `GET {gatewayUrl}/soyeht/events/{accountId}?token=...` (SSE)
|
|
11
|
+
- **Pairing**: QR code scanned by the app, then HTTP handshake
|
|
12
|
+
|
|
13
|
+
No WebRTC. No public domain required. Tailscale resolves connectivity.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
8
16
|
|
|
9
17
|
```bash
|
|
10
|
-
openclaw plugins install @soyeht/soyeht
|
|
18
|
+
openclaw plugins install @soyeht/soyeht --pin
|
|
11
19
|
openclaw plugins enable soyeht
|
|
12
20
|
```
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
Verify:
|
|
15
23
|
|
|
16
24
|
```bash
|
|
17
25
|
openclaw plugins list
|
|
@@ -19,9 +27,42 @@ openclaw plugins info soyeht
|
|
|
19
27
|
openclaw plugins doctor
|
|
20
28
|
```
|
|
21
29
|
|
|
22
|
-
##
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Set `gatewayUrl` to the URL accessible from the app. With Tailscale, this is your instance's Tailscale IP or MagicDNS hostname:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Using Tailscale IP
|
|
36
|
+
openclaw config set channels.soyeht.gatewayUrl "http://100.x.y.z:18789"
|
|
37
|
+
|
|
38
|
+
# Or using MagicDNS
|
|
39
|
+
openclaw config set channels.soyeht.gatewayUrl "http://my-machine.tailnet.ts.net:18789"
|
|
40
|
+
|
|
41
|
+
openclaw config set channels.soyeht.enabled true
|
|
42
|
+
openclaw gateway restart
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The plugin will auto-generate a pairing QR on startup if no peers are paired.
|
|
46
|
+
|
|
47
|
+
### Manual pairing
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
openclaw gateway call soyeht.security.pairing.start '{"accountId": "default", "allowOverwrite": true}'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Full config reference
|
|
23
54
|
|
|
24
|
-
|
|
55
|
+
```yaml
|
|
56
|
+
channels:
|
|
57
|
+
soyeht:
|
|
58
|
+
enabled: true
|
|
59
|
+
gatewayUrl: "http://100.x.y.z:18789" # required — your Tailscale/LAN URL
|
|
60
|
+
# Optional (only for legacy backend mode, not needed in V1):
|
|
61
|
+
# backendBaseUrl: "https://your-backend.example"
|
|
62
|
+
# pluginAuthToken: "your-token"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or with named accounts:
|
|
25
66
|
|
|
26
67
|
```yaml
|
|
27
68
|
channels:
|
|
@@ -29,45 +70,47 @@ channels:
|
|
|
29
70
|
accounts:
|
|
30
71
|
default:
|
|
31
72
|
enabled: true
|
|
32
|
-
|
|
33
|
-
pluginAuthToken: your-plugin-auth-token
|
|
34
|
-
security:
|
|
35
|
-
enabled: true
|
|
73
|
+
gatewayUrl: "http://100.x.y.z:18789"
|
|
36
74
|
```
|
|
37
75
|
|
|
38
|
-
##
|
|
76
|
+
## App endpoints
|
|
39
77
|
|
|
40
|
-
|
|
41
|
-
npm ci
|
|
42
|
-
npm run validate
|
|
43
|
-
```
|
|
78
|
+
All called by the Flutter app against `{gatewayUrl}`:
|
|
44
79
|
|
|
45
|
-
|
|
80
|
+
| Method | Path | Purpose |
|
|
81
|
+
|--------|------|---------|
|
|
82
|
+
| `GET` | `/soyeht/pairing/info?t={token}` | Fetch plugin keys for pairing |
|
|
83
|
+
| `POST` | `/soyeht/pairing/pair` | Register peer + start handshake |
|
|
84
|
+
| `POST` | `/soyeht/pairing/finish` | Complete handshake, get streamToken |
|
|
85
|
+
| `POST` | `/soyeht/messages/inbound` | Send encrypted message to agent |
|
|
86
|
+
| `GET` | `/soyeht/events/{accountId}?token={streamToken}` | SSE stream for agent replies |
|
|
87
|
+
| `GET` | `/soyeht/health` | Health check (503 if not ready) |
|
|
46
88
|
|
|
47
|
-
|
|
48
|
-
|
|
89
|
+
## QR format
|
|
90
|
+
|
|
91
|
+
The compact QR URL emitted by the plugin:
|
|
49
92
|
|
|
50
|
-
```bash
|
|
51
|
-
npm login
|
|
52
93
|
```
|
|
94
|
+
soyeht://pair?g={gatewayUrl}&t={pairingToken}&fp={fingerprint}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The app parses the URL, then calls the HTTP pairing endpoints above.
|
|
53
98
|
|
|
54
|
-
|
|
99
|
+
## Local development
|
|
55
100
|
|
|
56
101
|
```bash
|
|
57
|
-
npm
|
|
102
|
+
npm ci
|
|
103
|
+
npm run validate # typecheck + tests
|
|
104
|
+
npm run test:watch # vitest in watch mode
|
|
58
105
|
```
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
4. Publish publicly:
|
|
107
|
+
## Publish
|
|
63
108
|
|
|
64
109
|
```bash
|
|
110
|
+
npm version patch # bumps package.json + openclaw.plugin.json
|
|
65
111
|
npm publish
|
|
66
112
|
```
|
|
67
113
|
|
|
68
|
-
The package is configured with `publishConfig.access=public`, so the first publish of the scoped package does not need an extra `--access public`.
|
|
69
|
-
|
|
70
114
|
## Protocol docs
|
|
71
115
|
|
|
72
|
-
- [Protocol](docs/PROTOCOL.md)
|
|
73
|
-
- [Flutter agent prompt](docs/FLUTTER_AGENT_PROMPT.md)
|
|
116
|
+
- [Security Protocol](docs/PROTOCOL.md)
|
package/docs/PROTOCOL.md
CHANGED
|
@@ -67,13 +67,14 @@ Request params:
|
|
|
67
67
|
}
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Success payload:
|
|
70
|
+
Success payload (QR V2 — when `gatewayUrl` is configured):
|
|
71
71
|
|
|
72
72
|
```json
|
|
73
73
|
{
|
|
74
74
|
"qrPayload": {
|
|
75
|
-
"version":
|
|
75
|
+
"version": 2,
|
|
76
76
|
"type": "soyeht_pairing_qr",
|
|
77
|
+
"gatewayUrl": "http://100.x.y.z:18789",
|
|
77
78
|
"accountId": "default",
|
|
78
79
|
"pairingToken": "<base64url-32-bytes>",
|
|
79
80
|
"expiresAt": 1730000000000,
|
|
@@ -88,7 +89,13 @@ Success payload:
|
|
|
88
89
|
}
|
|
89
90
|
```
|
|
90
91
|
|
|
91
|
-
QR signature transcript:
|
|
92
|
+
QR V2 signature transcript:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
pairing_qr_v2|{gatewayUrl}|{accountId}|{pairingToken}|{expiresAt}|{allowOverwrite?1:0}|{pluginIdentityKey}|{pluginDhKey}|{fingerprint}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
QR V1 signature transcript (fallback — no `gatewayUrl`):
|
|
92
99
|
|
|
93
100
|
```text
|
|
94
101
|
pairing_qr_v1|{accountId}|{pairingToken}|{expiresAt}|{allowOverwrite?1:0}|{pluginIdentityKey}|{pluginDhKey}|{fingerprint}
|
|
@@ -96,6 +103,20 @@ pairing_qr_v1|{accountId}|{pairingToken}|{expiresAt}|{allowOverwrite?1:0}|{plugi
|
|
|
96
103
|
|
|
97
104
|
The signature is Ed25519 with the plugin identity private key.
|
|
98
105
|
|
|
106
|
+
### Compact QR URL
|
|
107
|
+
|
|
108
|
+
On service startup (auto-pairing) and via `pairing.start`, the plugin also emits a compact URL suitable for QR rendering:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
soyeht://pair?g={gatewayUrl}&t={pairingToken}&fp={fingerprint}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The app parses this URL, then calls the HTTP pairing endpoints to fetch full key material and complete pairing:
|
|
115
|
+
|
|
116
|
+
1. `GET {gatewayUrl}/soyeht/pairing/info?t={pairingToken}` — get plugin keys
|
|
117
|
+
2. `POST {gatewayUrl}/soyeht/pairing/pair` — register peer + start handshake
|
|
118
|
+
3. `POST {gatewayUrl}/soyeht/pairing/finish` — complete handshake, get streamToken
|
|
119
|
+
|
|
99
120
|
### `soyeht.security.pair`
|
|
100
121
|
|
|
101
122
|
Purpose:
|
|
@@ -373,16 +394,59 @@ Webhook/envelope:
|
|
|
373
394
|
- `direction_mismatch`
|
|
374
395
|
- `version_mismatch`
|
|
375
396
|
|
|
376
|
-
##
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
397
|
+
## V1 Transport: HTTP + SSE over Tailscale
|
|
398
|
+
|
|
399
|
+
In V1, the app and plugin communicate over a shared network (Tailscale). All endpoints use the `gatewayUrl` from config (e.g., `http://100.x.y.z:18789`).
|
|
400
|
+
|
|
401
|
+
### Inbound (app → plugin → agent)
|
|
402
|
+
|
|
403
|
+
```
|
|
404
|
+
POST {gatewayUrl}/soyeht/messages/inbound
|
|
405
|
+
Content-Type: application/json
|
|
406
|
+
|
|
407
|
+
{EnvelopeV2 JSON}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The plugin decrypts, responds `200`, then dispatches to the OpenClaw agent pipeline. The agent's reply is encrypted and pushed to the outbound SSE stream.
|
|
411
|
+
|
|
412
|
+
### Outbound (plugin → app via SSE)
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
GET {gatewayUrl}/soyeht/events/{accountId}?token={streamToken}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Or with header:
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
GET {gatewayUrl}/soyeht/events/{accountId}
|
|
422
|
+
Authorization: Bearer {streamToken}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The `streamToken` is returned by `POST /soyeht/pairing/finish` on successful handshake.
|
|
426
|
+
|
|
427
|
+
SSE events are encrypted EnvelopeV2 payloads. The app decrypts using its ratchet session state.
|
|
428
|
+
|
|
429
|
+
### Health check
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
GET {gatewayUrl}/soyeht/health
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Returns `200` with state info when ready, `503` when not.
|
|
436
|
+
|
|
437
|
+
## Recommended Client Flow (V1 — HTTP Pairing)
|
|
438
|
+
|
|
439
|
+
1. Plugin auto-generates QR on startup (or via `soyeht.security.pairing.start`)
|
|
440
|
+
2. App scans QR: `soyeht://pair?g={gatewayUrl}&t={pairingToken}&fp={fingerprint}`
|
|
441
|
+
3. App calls `GET {gatewayUrl}/soyeht/pairing/info?t={pairingToken}`
|
|
442
|
+
4. App verifies plugin keys and fingerprint
|
|
443
|
+
5. App generates long-term keys if needed
|
|
444
|
+
6. App calls `POST {gatewayUrl}/soyeht/pairing/pair` (registers peer + starts handshake)
|
|
445
|
+
7. App verifies `pluginSignature` from response
|
|
446
|
+
8. App signs the handshake transcript
|
|
447
|
+
9. App calls `POST {gatewayUrl}/soyeht/pairing/finish`
|
|
448
|
+
10. App receives `streamToken` and `sessionExpiresAt`
|
|
449
|
+
11. App derives X3DH session and connects to SSE: `GET {gatewayUrl}/soyeht/events/{accountId}?token={streamToken}`
|
|
450
|
+
12. App sends messages via `POST {gatewayUrl}/soyeht/messages/inbound`
|
|
451
|
+
13. App receives agent replies via SSE stream
|
|
388
452
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/http.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
3
|
import type { OpenClawPluginApi, PluginRuntimeChannel } from "openclaw/plugin-sdk";
|
|
3
4
|
import { normalizeAccountId, resolveSoyehtAccount } from "./config.js";
|
|
@@ -8,14 +9,22 @@ import type { SecurityV2Deps } from "./service.js";
|
|
|
8
9
|
import { PLUGIN_VERSION } from "./version.js";
|
|
9
10
|
import {
|
|
10
11
|
base64UrlDecode,
|
|
12
|
+
base64UrlEncode,
|
|
11
13
|
computeFingerprint,
|
|
14
|
+
ed25519Sign,
|
|
12
15
|
generateX25519KeyPair,
|
|
13
16
|
importEd25519PublicKey,
|
|
14
17
|
importX25519PublicKey,
|
|
15
18
|
ed25519Verify,
|
|
16
19
|
isTimestampValid,
|
|
17
20
|
} from "./crypto.js";
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
buildPairingProofTranscript,
|
|
23
|
+
buildPairingQrTranscript,
|
|
24
|
+
buildPairingQrTranscriptV2,
|
|
25
|
+
resolveGatewayUrl,
|
|
26
|
+
} from "./pairing.js";
|
|
27
|
+
import { renderQrTerminal } from "./qr.js";
|
|
19
28
|
import {
|
|
20
29
|
buildHandshakeTranscript,
|
|
21
30
|
signHandshakeTranscript,
|
|
@@ -537,6 +546,103 @@ function clearAccountSessionState(v2deps: SecurityV2Deps, accountId: string): vo
|
|
|
537
546
|
}
|
|
538
547
|
}
|
|
539
548
|
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// GET /soyeht/pairing/start — generate a pairing QR via HTTP (no RPC needed)
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
const DEFAULT_PAIRING_TTL_MS = 90_000;
|
|
554
|
+
|
|
555
|
+
export function pairingStartHandler(
|
|
556
|
+
api: OpenClawPluginApi,
|
|
557
|
+
v2deps: SecurityV2Deps,
|
|
558
|
+
) {
|
|
559
|
+
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
560
|
+
if (req.method !== "GET") {
|
|
561
|
+
sendJson(res, 405, { ok: false, error: "method_not_allowed" });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!v2deps.ready || !v2deps.identity) {
|
|
566
|
+
sendJson(res, 503, { ok: false, error: "service_unavailable" });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
571
|
+
const accountId = normalizeAccountId(url.searchParams.get("accountId") ?? "default");
|
|
572
|
+
const allowOverwrite = url.searchParams.get("allowOverwrite") !== "false";
|
|
573
|
+
const format = url.searchParams.get("format") ?? "json";
|
|
574
|
+
|
|
575
|
+
const { allowed } = v2deps.rateLimiter.check(`pairing:start:http:${accountId}`);
|
|
576
|
+
if (!allowed) {
|
|
577
|
+
sendJson(res, 429, { ok: false, error: "rate_limited" });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (v2deps.peers.has(accountId) && !allowOverwrite) {
|
|
582
|
+
sendJson(res, 409, { ok: false, error: "peer_already_paired" });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Clear stale pairing sessions for this account
|
|
587
|
+
for (const [token, session] of v2deps.pairingSessions) {
|
|
588
|
+
if (session.accountId === accountId) {
|
|
589
|
+
v2deps.pairingSessions.delete(token);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const pairingToken = base64UrlEncode(randomBytes(32));
|
|
594
|
+
const expiresAt = Date.now() + DEFAULT_PAIRING_TTL_MS;
|
|
595
|
+
const fingerprint = computeFingerprint(v2deps.identity);
|
|
596
|
+
|
|
597
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
598
|
+
const account = resolveSoyehtAccount(cfg, accountId);
|
|
599
|
+
const gatewayUrl = resolveGatewayUrl(api, account.gatewayUrl);
|
|
600
|
+
|
|
601
|
+
const basePayload = {
|
|
602
|
+
accountId,
|
|
603
|
+
pairingToken,
|
|
604
|
+
expiresAt,
|
|
605
|
+
allowOverwrite,
|
|
606
|
+
pluginIdentityKey: v2deps.identity.signKey.publicKeyB64,
|
|
607
|
+
pluginDhKey: v2deps.identity.dhKey.publicKeyB64,
|
|
608
|
+
fingerprint,
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
let qrPayload: Record<string, unknown>;
|
|
612
|
+
if (gatewayUrl) {
|
|
613
|
+
const transcript = buildPairingQrTranscriptV2({ gatewayUrl, ...basePayload });
|
|
614
|
+
const signature = base64UrlEncode(ed25519Sign(v2deps.identity.signKey.privateKey, transcript));
|
|
615
|
+
qrPayload = { version: 2, type: "soyeht_pairing_qr", gatewayUrl, ...basePayload, signature };
|
|
616
|
+
} else {
|
|
617
|
+
const transcript = buildPairingQrTranscript(basePayload);
|
|
618
|
+
const signature = base64UrlEncode(ed25519Sign(v2deps.identity.signKey.privateKey, transcript));
|
|
619
|
+
qrPayload = { version: 1, type: "soyeht_pairing_qr", ...basePayload, signature };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
v2deps.pairingSessions.set(pairingToken, {
|
|
623
|
+
token: pairingToken,
|
|
624
|
+
accountId,
|
|
625
|
+
expiresAt,
|
|
626
|
+
allowOverwrite,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const qrUrl = `soyeht://pair?g=${gatewayUrl}&t=${pairingToken}&fp=${fingerprint}`;
|
|
630
|
+
api.logger.info("[soyeht] Pairing started via HTTP", { accountId, expiresAt });
|
|
631
|
+
|
|
632
|
+
if (format === "terminal") {
|
|
633
|
+
const rendered = renderQrTerminal(qrUrl);
|
|
634
|
+
const text = rendered
|
|
635
|
+
? `\n${rendered}\n\n${qrUrl}\n\nExpires in ${DEFAULT_PAIRING_TTL_MS / 1000}s\n`
|
|
636
|
+
: `QR too large for terminal.\n\n${qrUrl}\n\nExpires in ${DEFAULT_PAIRING_TTL_MS / 1000}s\n`;
|
|
637
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
638
|
+
res.end(text);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
sendJson(res, 200, { ok: true, qrUrl, qrPayload, expiresAt });
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
540
646
|
// ---------------------------------------------------------------------------
|
|
541
647
|
// GET /soyeht/pairing/info?t=<pairingToken>
|
|
542
648
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
livekitTokenHandler,
|
|
16
16
|
inboundHandler,
|
|
17
17
|
sseHandler,
|
|
18
|
+
pairingStartHandler,
|
|
18
19
|
pairingInfoHandler,
|
|
19
20
|
pairingPairHandler,
|
|
20
21
|
pairingFinishHandler,
|
|
@@ -131,6 +132,11 @@ const soyehtPlugin: OpenClawPluginDefinition = {
|
|
|
131
132
|
});
|
|
132
133
|
|
|
133
134
|
// HTTP pairing routes (app pairs via HTTP, no WebSocket needed)
|
|
135
|
+
api.registerHttpRoute({
|
|
136
|
+
path: "/soyeht/pairing/start",
|
|
137
|
+
auth: "plugin",
|
|
138
|
+
handler: pairingStartHandler(api, v2deps),
|
|
139
|
+
});
|
|
134
140
|
api.registerHttpRoute({
|
|
135
141
|
path: "/soyeht/pairing/info",
|
|
136
142
|
auth: "plugin",
|
package/src/outbound-queue.ts
CHANGED
|
@@ -95,6 +95,19 @@ export function createOutboundQueue(opts: OutboundQueueOptions = {}): OutboundQu
|
|
|
95
95
|
let waiting: ((value: IteratorResult<QueueEntry>) => void) | null = null;
|
|
96
96
|
let closed = false;
|
|
97
97
|
|
|
98
|
+
// Drain backlog: snapshot current queue entries still within TTL.
|
|
99
|
+
// Done synchronously before registering live subscriber — no race in
|
|
100
|
+
// single-threaded Node.js, so no duplicates and no missed messages.
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const backlog = queues.get(accountId);
|
|
103
|
+
if (backlog) {
|
|
104
|
+
for (const entry of backlog) {
|
|
105
|
+
if (now - entry.enqueuedAt < ttlMs) {
|
|
106
|
+
pending.push(entry);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
const sub: Subscriber = {
|
|
99
112
|
accountId,
|
|
100
113
|
push(entry: QueueEntry) {
|
package/src/pairing.ts
CHANGED
|
@@ -119,7 +119,7 @@ function clearAccountSessionState(v2deps: SecurityV2Deps, accountId: string): vo
|
|
|
119
119
|
// Resolve the gateway URL for QR V2
|
|
120
120
|
// ---------------------------------------------------------------------------
|
|
121
121
|
|
|
122
|
-
function resolveGatewayUrl(api: OpenClawPluginApi, configGatewayUrl: string): string {
|
|
122
|
+
export function resolveGatewayUrl(api: OpenClawPluginApi, configGatewayUrl: string): string {
|
|
123
123
|
// 1) Explicit config
|
|
124
124
|
if (configGatewayUrl) return configGatewayUrl;
|
|
125
125
|
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PLUGIN_VERSION = "0.2.
|
|
1
|
+
export const PLUGIN_VERSION = "0.2.11";
|