@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.
- 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/pairing.ts +1 -1
- package/src/version.ts +1 -1
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/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";
|