@soyeht/soyeht 0.2.8 → 0.2.10
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/config.ts +30 -4
- package/src/http.ts +16 -2
- package/src/index.ts +1 -1
- package/src/outbound-queue.ts +24 -0
- package/src/types.ts +46 -40
- 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/config.ts
CHANGED
|
@@ -71,14 +71,32 @@ function readAccountsSection(
|
|
|
71
71
|
return section?.["accounts"] as Record<string, unknown> | undefined;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Read flat config from `channels.soyeht.*` (all keys except "accounts").
|
|
76
|
+
* Used as defaults when no explicit account entry exists.
|
|
77
|
+
*/
|
|
78
|
+
function readFlatConfig(cfg: OpenClawConfig): Record<string, unknown> {
|
|
79
|
+
const section = readConfigSection(cfg);
|
|
80
|
+
if (!section) return {};
|
|
81
|
+
const flat: Record<string, unknown> = {};
|
|
82
|
+
for (const [key, value] of Object.entries(section)) {
|
|
83
|
+
if (key !== "accounts") {
|
|
84
|
+
flat[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return flat;
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
function readAccountConfig(
|
|
75
91
|
cfg: OpenClawConfig,
|
|
76
92
|
accountId: string,
|
|
77
93
|
): Record<string, unknown> {
|
|
94
|
+
const flat = readFlatConfig(cfg);
|
|
78
95
|
const accounts = readAccountsSection(cfg);
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
96
|
+
const entry = accounts?.[accountId];
|
|
97
|
+
const accountRaw = entry && typeof entry === "object" ? (entry as Record<string, unknown>) : {};
|
|
98
|
+
// Merge: flat config as defaults, account-specific overrides
|
|
99
|
+
return { ...flat, ...accountRaw };
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
// ---------------------------------------------------------------------------
|
|
@@ -199,8 +217,16 @@ export function resolveSoyehtAccount(
|
|
|
199
217
|
|
|
200
218
|
export function listSoyehtAccountIds(cfg: OpenClawConfig): string[] {
|
|
201
219
|
const accounts = readAccountsSection(cfg);
|
|
202
|
-
|
|
220
|
+
const ids = listConfiguredAccountIds({
|
|
203
221
|
accounts: accounts as Record<string, unknown> | undefined,
|
|
204
222
|
normalizeAccountId,
|
|
205
223
|
});
|
|
224
|
+
// If no explicit accounts but flat config exists, treat as "default" account
|
|
225
|
+
if (ids.length === 0) {
|
|
226
|
+
const flat = readFlatConfig(cfg);
|
|
227
|
+
if (Object.keys(flat).length > 0) {
|
|
228
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return ids;
|
|
206
232
|
}
|
package/src/http.ts
CHANGED
|
@@ -126,9 +126,23 @@ export function processInboundEnvelope(
|
|
|
126
126
|
// GET /soyeht/health
|
|
127
127
|
// ---------------------------------------------------------------------------
|
|
128
128
|
|
|
129
|
-
export function healthHandler(_api: OpenClawPluginApi) {
|
|
129
|
+
export function healthHandler(_api: OpenClawPluginApi, v2deps?: SecurityV2Deps) {
|
|
130
130
|
return async (_req: IncomingMessage, res: ServerResponse) => {
|
|
131
|
-
|
|
131
|
+
const ready = v2deps?.ready ?? false;
|
|
132
|
+
const identityLoaded = Boolean(v2deps?.identity);
|
|
133
|
+
const activeSessions = v2deps?.sessions.size ?? 0;
|
|
134
|
+
const pairedPeers = v2deps?.peers.size ?? 0;
|
|
135
|
+
const queueStats = v2deps?.outboundQueue.stats() ?? { accounts: 0, totalMessages: 0 };
|
|
136
|
+
|
|
137
|
+
sendJson(res, ready ? 200 : 503, {
|
|
138
|
+
ok: ready,
|
|
139
|
+
plugin: "soyeht",
|
|
140
|
+
version: PLUGIN_VERSION,
|
|
141
|
+
identity: identityLoaded,
|
|
142
|
+
peers: pairedPeers,
|
|
143
|
+
sessions: activeSessions,
|
|
144
|
+
queue: queueStats,
|
|
145
|
+
});
|
|
132
146
|
};
|
|
133
147
|
}
|
|
134
148
|
|
package/src/index.ts
CHANGED
|
@@ -104,7 +104,7 @@ const soyehtPlugin: OpenClawPluginDefinition = {
|
|
|
104
104
|
api.registerHttpRoute({
|
|
105
105
|
path: "/soyeht/health",
|
|
106
106
|
auth: "plugin",
|
|
107
|
-
handler: healthHandler(api),
|
|
107
|
+
handler: healthHandler(api, v2deps),
|
|
108
108
|
});
|
|
109
109
|
api.registerHttpRoute({
|
|
110
110
|
path: "/soyeht/webhook/deliver",
|
package/src/outbound-queue.ts
CHANGED
|
@@ -41,6 +41,8 @@ export type OutboundQueue = {
|
|
|
41
41
|
prune(): number;
|
|
42
42
|
clear(): void;
|
|
43
43
|
|
|
44
|
+
stats(): { accounts: number; totalMessages: number };
|
|
45
|
+
|
|
44
46
|
// Stream token management (for SSE auth)
|
|
45
47
|
createStreamToken(accountId: string, expiresAt: number): string;
|
|
46
48
|
validateStreamToken(token: string): StreamTokenInfo | null;
|
|
@@ -93,6 +95,19 @@ export function createOutboundQueue(opts: OutboundQueueOptions = {}): OutboundQu
|
|
|
93
95
|
let waiting: ((value: IteratorResult<QueueEntry>) => void) | null = null;
|
|
94
96
|
let closed = false;
|
|
95
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
|
+
|
|
96
111
|
const sub: Subscriber = {
|
|
97
112
|
accountId,
|
|
98
113
|
push(entry: QueueEntry) {
|
|
@@ -217,12 +232,21 @@ export function createOutboundQueue(opts: OutboundQueueOptions = {}): OutboundQu
|
|
|
217
232
|
}
|
|
218
233
|
}
|
|
219
234
|
|
|
235
|
+
function stats(): { accounts: number; totalMessages: number } {
|
|
236
|
+
let totalMessages = 0;
|
|
237
|
+
for (const q of queues.values()) {
|
|
238
|
+
totalMessages += q.length;
|
|
239
|
+
}
|
|
240
|
+
return { accounts: queues.size, totalMessages };
|
|
241
|
+
}
|
|
242
|
+
|
|
220
243
|
return {
|
|
221
244
|
enqueue,
|
|
222
245
|
subscribe,
|
|
223
246
|
hasSubscribers,
|
|
224
247
|
prune,
|
|
225
248
|
clear,
|
|
249
|
+
stats,
|
|
226
250
|
createStreamToken,
|
|
227
251
|
validateStreamToken,
|
|
228
252
|
revokeStreamTokensForAccount,
|
package/src/types.ts
CHANGED
|
@@ -31,54 +31,57 @@ export type SoyehtAccountConfig = {
|
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
transcribeInbound: { type: "boolean" },
|
|
48
|
-
ttsOutbound: { type: "boolean" },
|
|
49
|
-
},
|
|
34
|
+
// Shared properties object — reused at account level and channel top level
|
|
35
|
+
const accountConfigProperties: Record<string, unknown> = {
|
|
36
|
+
enabled: { type: "boolean" },
|
|
37
|
+
backendBaseUrl: { type: "string" },
|
|
38
|
+
pluginAuthToken: { type: "string" },
|
|
39
|
+
gatewayUrl: { type: "string" },
|
|
40
|
+
allowProactive: { type: "boolean" },
|
|
41
|
+
audio: {
|
|
42
|
+
type: "object",
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
properties: {
|
|
45
|
+
transcribeInbound: { type: "boolean" },
|
|
46
|
+
ttsOutbound: { type: "boolean" },
|
|
50
47
|
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
},
|
|
48
|
+
},
|
|
49
|
+
files: {
|
|
50
|
+
type: "object",
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
properties: {
|
|
53
|
+
acceptInbound: { type: "boolean" },
|
|
54
|
+
maxBytes: { type: "number" },
|
|
58
55
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
56
|
+
},
|
|
57
|
+
security: {
|
|
58
|
+
type: "object",
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
properties: {
|
|
61
|
+
enabled: { type: "boolean" },
|
|
62
|
+
timestampToleranceMs: { type: "number" },
|
|
63
|
+
dhRatchetIntervalMessages: { type: "number" },
|
|
64
|
+
dhRatchetIntervalMs: { type: "number" },
|
|
65
|
+
sessionMaxAgeMs: { type: "number" },
|
|
66
|
+
rateLimit: {
|
|
67
|
+
type: "object",
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
properties: {
|
|
70
|
+
maxRequests: { type: "number" },
|
|
71
|
+
windowMs: { type: "number" },
|
|
75
72
|
},
|
|
76
73
|
},
|
|
77
74
|
},
|
|
78
75
|
},
|
|
79
76
|
};
|
|
80
77
|
|
|
81
|
-
export
|
|
78
|
+
export const SoyehtAccountConfigSchema: JsonSchema = {
|
|
79
|
+
type: "object",
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: accountConfigProperties,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type SoyehtChannelConfig = SoyehtAccountConfig & {
|
|
82
85
|
accounts?: Record<string, SoyehtAccountConfig>;
|
|
83
86
|
};
|
|
84
87
|
|
|
@@ -86,6 +89,9 @@ export const SoyehtChannelConfigSchema: JsonSchema = {
|
|
|
86
89
|
type: "object",
|
|
87
90
|
additionalProperties: false,
|
|
88
91
|
properties: {
|
|
92
|
+
// Top-level account fields (flat config shorthand for single-account setups)
|
|
93
|
+
...accountConfigProperties,
|
|
94
|
+
// Named accounts (multi-account support)
|
|
89
95
|
accounts: {
|
|
90
96
|
type: "object",
|
|
91
97
|
additionalProperties: SoyehtAccountConfigSchema,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PLUGIN_VERSION = "0.2.
|
|
1
|
+
export const PLUGIN_VERSION = "0.2.10";
|