@soyeht/soyeht 0.2.10 → 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.
@@ -5,7 +5,7 @@
5
5
  ],
6
6
  "name": "Soyeht",
7
7
  "description": "Channel plugin for the Soyeht Flutter mobile app",
8
- "version": "0.2.10",
8
+ "version": "0.2.11",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soyeht/soyeht",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "OpenClaw channel plugin for the Soyeht Flutter mobile app",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
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 { buildPairingProofTranscript } from "./pairing.js";
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/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.10";
1
+ export const PLUGIN_VERSION = "0.2.11";