@soyeht/soyeht 0.2.2 → 0.2.3
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 +455 -2
- package/src/index.ts +20 -0
- package/src/service.ts +5 -8
- package/src/version.ts +1 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/http.ts
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
-
import { normalizeAccountId } from "./config.js";
|
|
3
|
+
import { normalizeAccountId, resolveSoyehtAccount } from "./config.js";
|
|
4
4
|
import { decryptEnvelopeV2, validateEnvelopeV2, type EnvelopeV2 } from "./envelope-v2.js";
|
|
5
|
-
import { cloneRatchetSession } from "./ratchet.js";
|
|
5
|
+
import { cloneRatchetSession, zeroBuffer, type RatchetState } from "./ratchet.js";
|
|
6
6
|
import type { SecurityV2Deps } from "./service.js";
|
|
7
7
|
import { PLUGIN_VERSION } from "./version.js";
|
|
8
|
+
import {
|
|
9
|
+
base64UrlDecode,
|
|
10
|
+
computeFingerprint,
|
|
11
|
+
generateX25519KeyPair,
|
|
12
|
+
importEd25519PublicKey,
|
|
13
|
+
importX25519PublicKey,
|
|
14
|
+
ed25519Verify,
|
|
15
|
+
isTimestampValid,
|
|
16
|
+
} from "./crypto.js";
|
|
17
|
+
import { buildPairingProofTranscript } from "./pairing.js";
|
|
18
|
+
import {
|
|
19
|
+
buildHandshakeTranscript,
|
|
20
|
+
signHandshakeTranscript,
|
|
21
|
+
verifyHandshakeTranscript,
|
|
22
|
+
performX3DH,
|
|
23
|
+
} from "./x3dh.js";
|
|
24
|
+
import { savePeer, saveSession, deleteSession, type PeerIdentity } from "./identity.js";
|
|
8
25
|
|
|
9
26
|
// ---------------------------------------------------------------------------
|
|
10
27
|
// Helpers
|
|
@@ -341,6 +358,442 @@ export function sseHandler(
|
|
|
341
358
|
};
|
|
342
359
|
}
|
|
343
360
|
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Helpers for HTTP pairing
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
function isWellFormedNonce(nonce: string): boolean {
|
|
366
|
+
try {
|
|
367
|
+
const decoded = base64UrlDecode(nonce);
|
|
368
|
+
return decoded.length >= 16 && decoded.length <= 64;
|
|
369
|
+
} catch {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function clearAccountSessionState(v2deps: SecurityV2Deps, accountId: string): void {
|
|
375
|
+
const existing = v2deps.sessions.get(accountId);
|
|
376
|
+
if (existing) {
|
|
377
|
+
zeroBuffer(existing.rootKey);
|
|
378
|
+
zeroBuffer(existing.sending.chainKey);
|
|
379
|
+
zeroBuffer(existing.receiving.chainKey);
|
|
380
|
+
v2deps.sessions.delete(accountId);
|
|
381
|
+
}
|
|
382
|
+
for (const [key, pending] of v2deps.pendingHandshakes) {
|
|
383
|
+
if (pending.accountId === accountId) {
|
|
384
|
+
v2deps.pendingHandshakes.delete(key);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// GET /soyeht/pairing/info?t=<pairingToken>
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
export function pairingInfoHandler(
|
|
394
|
+
_api: OpenClawPluginApi,
|
|
395
|
+
v2deps: SecurityV2Deps,
|
|
396
|
+
) {
|
|
397
|
+
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
398
|
+
if (req.method !== "GET") {
|
|
399
|
+
sendJson(res, 405, { ok: false, error: "method_not_allowed" });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!v2deps.ready || !v2deps.identity) {
|
|
404
|
+
sendJson(res, 503, { ok: false, error: "service_unavailable" });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
409
|
+
const pairingToken = url.searchParams.get("t") ?? "";
|
|
410
|
+
if (!pairingToken) {
|
|
411
|
+
sendJson(res, 400, { ok: false, error: "missing_pairing_token" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const { allowed } = v2deps.rateLimiter.check(`pairing:info:${pairingToken}`);
|
|
416
|
+
if (!allowed) {
|
|
417
|
+
sendJson(res, 429, { ok: false, error: "rate_limited" });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const session = v2deps.pairingSessions.get(pairingToken);
|
|
422
|
+
if (!session) {
|
|
423
|
+
sendJson(res, 404, { ok: false, error: "pairing_not_found" });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (session.expiresAt <= Date.now()) {
|
|
428
|
+
sendJson(res, 410, { ok: false, error: "pairing_expired" });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
sendJson(res, 200, {
|
|
433
|
+
ok: true,
|
|
434
|
+
accountId: session.accountId,
|
|
435
|
+
pluginIdentityKey: v2deps.identity.signKey.publicKeyB64,
|
|
436
|
+
pluginDhKey: v2deps.identity.dhKey.publicKeyB64,
|
|
437
|
+
fingerprint: computeFingerprint(v2deps.identity),
|
|
438
|
+
expiresAt: session.expiresAt,
|
|
439
|
+
allowOverwrite: session.allowOverwrite,
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// POST /soyeht/pairing/pair — register peer + start handshake in one step
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
const PAIRING_HANDSHAKE_TOLERANCE_MS = 120_000; // 2 minutes for HTTP round-trip
|
|
449
|
+
|
|
450
|
+
export function pairingPairHandler(
|
|
451
|
+
api: OpenClawPluginApi,
|
|
452
|
+
v2deps: SecurityV2Deps,
|
|
453
|
+
) {
|
|
454
|
+
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
455
|
+
if (req.method !== "POST") {
|
|
456
|
+
sendJson(res, 405, { ok: false, error: "method_not_allowed" });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!v2deps.ready || !v2deps.identity) {
|
|
461
|
+
sendJson(res, 503, { ok: false, error: "service_unavailable" });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let body: Record<string, unknown>;
|
|
466
|
+
try {
|
|
467
|
+
const raw = await readRawBodyBuffer(req);
|
|
468
|
+
body = JSON.parse(raw.toString("utf8"));
|
|
469
|
+
} catch {
|
|
470
|
+
sendJson(res, 400, { ok: false, error: "invalid_json" });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const pairingToken = body["pairingToken"] as string | undefined;
|
|
475
|
+
const accountId = normalizeAccountId(body["accountId"] as string | undefined);
|
|
476
|
+
const appIdentityKey = body["appIdentityKey"] as string | undefined;
|
|
477
|
+
const appDhKey = body["appDhKey"] as string | undefined;
|
|
478
|
+
const appSignature = body["appSignature"] as string | undefined;
|
|
479
|
+
const appEphemeralKey = body["appEphemeralKey"] as string | undefined;
|
|
480
|
+
const nonce = body["nonce"] as string | undefined;
|
|
481
|
+
const timestamp = body["timestamp"] as number | undefined;
|
|
482
|
+
|
|
483
|
+
if (!pairingToken || !appIdentityKey || !appDhKey || !appSignature ||
|
|
484
|
+
!appEphemeralKey || !nonce || typeof timestamp !== "number") {
|
|
485
|
+
sendJson(res, 400, { ok: false, error: "missing_params" });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const { allowed } = v2deps.rateLimiter.check(`pairing:pair:${accountId}`);
|
|
490
|
+
if (!allowed) {
|
|
491
|
+
sendJson(res, 429, { ok: false, error: "rate_limited" });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- Validate pairing session ---
|
|
496
|
+
|
|
497
|
+
const pairingSession = v2deps.pairingSessions.get(pairingToken);
|
|
498
|
+
if (!pairingSession) {
|
|
499
|
+
sendJson(res, 404, { ok: false, error: "pairing_not_found" });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (pairingSession.expiresAt <= Date.now()) {
|
|
504
|
+
v2deps.pairingSessions.delete(pairingToken);
|
|
505
|
+
sendJson(res, 410, { ok: false, error: "pairing_expired" });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (pairingSession.accountId !== accountId) {
|
|
510
|
+
sendJson(res, 403, { ok: false, error: "account_mismatch" });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// --- Validate keys ---
|
|
515
|
+
|
|
516
|
+
let appIdentityPub;
|
|
517
|
+
try {
|
|
518
|
+
appIdentityPub = importEd25519PublicKey(appIdentityKey);
|
|
519
|
+
} catch {
|
|
520
|
+
sendJson(res, 400, { ok: false, error: "invalid_identity_key" });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
importX25519PublicKey(appDhKey);
|
|
526
|
+
} catch {
|
|
527
|
+
sendJson(res, 400, { ok: false, error: "invalid_dh_key" });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
importX25519PublicKey(appEphemeralKey);
|
|
533
|
+
} catch {
|
|
534
|
+
sendJson(res, 400, { ok: false, error: "invalid_ephemeral_key" });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// --- Verify pairing signature ---
|
|
539
|
+
|
|
540
|
+
const proofTranscript = buildPairingProofTranscript({
|
|
541
|
+
accountId,
|
|
542
|
+
pairingToken,
|
|
543
|
+
expiresAt: pairingSession.expiresAt,
|
|
544
|
+
appIdentityKey,
|
|
545
|
+
appDhKey,
|
|
546
|
+
});
|
|
547
|
+
if (!ed25519Verify(appIdentityPub, proofTranscript, base64UrlDecode(appSignature))) {
|
|
548
|
+
sendJson(res, 403, { ok: false, error: "invalid_signature" });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// --- Validate nonce + timestamp ---
|
|
553
|
+
|
|
554
|
+
if (!isWellFormedNonce(nonce)) {
|
|
555
|
+
sendJson(res, 400, { ok: false, error: "invalid_nonce" });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!isTimestampValid(timestamp, PAIRING_HANDSHAKE_TOLERANCE_MS)) {
|
|
560
|
+
sendJson(res, 400, { ok: false, error: "timestamp_out_of_range" });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!v2deps.nonceCache.add(`pairing:http:${accountId}:${nonce}`)) {
|
|
565
|
+
sendJson(res, 409, { ok: false, error: "nonce_reused" });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// --- Check existing peer ---
|
|
570
|
+
|
|
571
|
+
if (v2deps.peers.has(accountId) && !pairingSession.allowOverwrite) {
|
|
572
|
+
sendJson(res, 409, { ok: false, error: "peer_already_paired" });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// --- Register peer (clear old state first) ---
|
|
577
|
+
|
|
578
|
+
clearAccountSessionState(v2deps, accountId);
|
|
579
|
+
if (v2deps.stateDir) {
|
|
580
|
+
await deleteSession(v2deps.stateDir, accountId).catch(() => {});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const peer: PeerIdentity = {
|
|
584
|
+
accountId,
|
|
585
|
+
identityKeyB64: appIdentityKey,
|
|
586
|
+
dhKeyB64: appDhKey,
|
|
587
|
+
};
|
|
588
|
+
v2deps.peers.set(accountId, peer);
|
|
589
|
+
v2deps.pairingSessions.delete(pairingToken);
|
|
590
|
+
|
|
591
|
+
if (v2deps.stateDir) {
|
|
592
|
+
await savePeer(v2deps.stateDir, peer).catch(() => {});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// --- Start handshake ---
|
|
596
|
+
|
|
597
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
598
|
+
const account = resolveSoyehtAccount(cfg, accountId);
|
|
599
|
+
const now = Date.now();
|
|
600
|
+
const challengeExpiresAt = now + PAIRING_HANDSHAKE_TOLERANCE_MS;
|
|
601
|
+
const sessionExpiresAt = now + account.security.sessionMaxAgeMs;
|
|
602
|
+
const pluginEphemeralKey = generateX25519KeyPair();
|
|
603
|
+
|
|
604
|
+
const transcript = buildHandshakeTranscript({
|
|
605
|
+
accountId,
|
|
606
|
+
appEphKeyB64: appEphemeralKey,
|
|
607
|
+
pluginEphKeyB64: pluginEphemeralKey.publicKeyB64,
|
|
608
|
+
nonce,
|
|
609
|
+
timestamp,
|
|
610
|
+
expiresAt: sessionExpiresAt,
|
|
611
|
+
});
|
|
612
|
+
const pluginSignature = signHandshakeTranscript(
|
|
613
|
+
v2deps.identity.signKey.privateKey,
|
|
614
|
+
transcript,
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
const pendingKey = `${accountId}:${nonce}`;
|
|
618
|
+
v2deps.pendingHandshakes.set(pendingKey, {
|
|
619
|
+
key: pendingKey,
|
|
620
|
+
accountId,
|
|
621
|
+
nonce,
|
|
622
|
+
appEphemeralKey,
|
|
623
|
+
pluginEphemeralKey,
|
|
624
|
+
transcript,
|
|
625
|
+
challengeExpiresAt,
|
|
626
|
+
sessionExpiresAt,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
api.logger.info("[soyeht] HTTP pairing + handshake init completed", { accountId });
|
|
630
|
+
|
|
631
|
+
sendJson(res, 200, {
|
|
632
|
+
ok: true,
|
|
633
|
+
accountId,
|
|
634
|
+
pluginIdentityKey: v2deps.identity.signKey.publicKeyB64,
|
|
635
|
+
pluginDhKey: v2deps.identity.dhKey.publicKeyB64,
|
|
636
|
+
fingerprint: computeFingerprint(v2deps.identity),
|
|
637
|
+
pluginEphemeralKey: pluginEphemeralKey.publicKeyB64,
|
|
638
|
+
pluginSignature,
|
|
639
|
+
nonce,
|
|
640
|
+
timestamp,
|
|
641
|
+
serverTimestamp: now,
|
|
642
|
+
challengeExpiresAt,
|
|
643
|
+
sessionExpiresAt,
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
// POST /soyeht/pairing/finish — complete handshake, derive session, return streamToken
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
export function pairingFinishHandler(
|
|
653
|
+
api: OpenClawPluginApi,
|
|
654
|
+
v2deps: SecurityV2Deps,
|
|
655
|
+
) {
|
|
656
|
+
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
657
|
+
if (req.method !== "POST") {
|
|
658
|
+
sendJson(res, 405, { ok: false, error: "method_not_allowed" });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!v2deps.ready || !v2deps.identity) {
|
|
663
|
+
sendJson(res, 503, { ok: false, error: "service_unavailable" });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let body: Record<string, unknown>;
|
|
668
|
+
try {
|
|
669
|
+
const raw = await readRawBodyBuffer(req);
|
|
670
|
+
body = JSON.parse(raw.toString("utf8"));
|
|
671
|
+
} catch {
|
|
672
|
+
sendJson(res, 400, { ok: false, error: "invalid_json" });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const accountId = normalizeAccountId(body["accountId"] as string | undefined);
|
|
677
|
+
const nonce = body["nonce"] as string | undefined;
|
|
678
|
+
const appSignature = body["appSignature"] as string | undefined;
|
|
679
|
+
|
|
680
|
+
if (!nonce || !appSignature) {
|
|
681
|
+
sendJson(res, 400, { ok: false, error: "missing_params" });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const { allowed } = v2deps.rateLimiter.check(`pairing:finish:${accountId}`);
|
|
686
|
+
if (!allowed) {
|
|
687
|
+
sendJson(res, 429, { ok: false, error: "rate_limited" });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const pendingKey = `${accountId}:${nonce}`;
|
|
692
|
+
const pending = v2deps.pendingHandshakes.get(pendingKey);
|
|
693
|
+
if (!pending) {
|
|
694
|
+
sendJson(res, 404, { ok: false, error: "handshake_not_found" });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (pending.challengeExpiresAt <= Date.now()) {
|
|
699
|
+
v2deps.pendingHandshakes.delete(pendingKey);
|
|
700
|
+
sendJson(res, 410, { ok: false, error: "handshake_expired" });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const peer = v2deps.peers.get(accountId);
|
|
705
|
+
if (!peer) {
|
|
706
|
+
sendJson(res, 404, { ok: false, error: "peer_not_found" });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let appIdentityPub;
|
|
711
|
+
try {
|
|
712
|
+
appIdentityPub = importEd25519PublicKey(peer.identityKeyB64);
|
|
713
|
+
} catch {
|
|
714
|
+
sendJson(res, 500, { ok: false, error: "invalid_peer_key" });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!verifyHandshakeTranscript(appIdentityPub, pending.transcript, appSignature)) {
|
|
719
|
+
sendJson(res, 403, { ok: false, error: "invalid_signature" });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
let appEphemeralPub;
|
|
724
|
+
try {
|
|
725
|
+
appEphemeralPub = importX25519PublicKey(pending.appEphemeralKey);
|
|
726
|
+
} catch {
|
|
727
|
+
v2deps.pendingHandshakes.delete(pendingKey);
|
|
728
|
+
sendJson(res, 500, { ok: false, error: "invalid_ephemeral_key" });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let peerStaticDhPub;
|
|
733
|
+
try {
|
|
734
|
+
peerStaticDhPub = importX25519PublicKey(peer.dhKeyB64);
|
|
735
|
+
} catch {
|
|
736
|
+
sendJson(res, 500, { ok: false, error: "invalid_peer_dh_key" });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const x3dhResult = await performX3DH({
|
|
741
|
+
myStaticDhKey: v2deps.identity.dhKey,
|
|
742
|
+
myEphemeralKey: pending.pluginEphemeralKey,
|
|
743
|
+
peerStaticDhPub,
|
|
744
|
+
peerEphemeralPub: appEphemeralPub,
|
|
745
|
+
nonce,
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// Clear any existing session for this account
|
|
749
|
+
const existingSession = v2deps.sessions.get(accountId);
|
|
750
|
+
if (existingSession) {
|
|
751
|
+
zeroBuffer(existingSession.rootKey);
|
|
752
|
+
zeroBuffer(existingSession.sending.chainKey);
|
|
753
|
+
zeroBuffer(existingSession.receiving.chainKey);
|
|
754
|
+
v2deps.sessions.delete(accountId);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const session: RatchetState = {
|
|
758
|
+
accountId,
|
|
759
|
+
rootKey: x3dhResult.rootKey,
|
|
760
|
+
sending: { chainKey: x3dhResult.sendChainKey, counter: 0 },
|
|
761
|
+
receiving: { chainKey: x3dhResult.recvChainKey, counter: 0 },
|
|
762
|
+
myCurrentEphDhKey: pending.pluginEphemeralKey,
|
|
763
|
+
peerLastEphDhKeyB64: pending.appEphemeralKey,
|
|
764
|
+
dhRatchetSendCount: 0,
|
|
765
|
+
dhRatchetRecvCount: 0,
|
|
766
|
+
createdAt: Date.now(),
|
|
767
|
+
expiresAt: pending.sessionExpiresAt,
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
v2deps.sessions.set(accountId, session);
|
|
771
|
+
v2deps.pendingHandshakes.delete(pendingKey);
|
|
772
|
+
|
|
773
|
+
if (v2deps.stateDir) {
|
|
774
|
+
await saveSession(v2deps.stateDir, session).catch((err) => {
|
|
775
|
+
api.logger.error("[soyeht] Failed to persist session", { accountId, err });
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Issue stream token for SSE auth
|
|
780
|
+
v2deps.outboundQueue.revokeStreamTokensForAccount(accountId);
|
|
781
|
+
const streamToken = v2deps.outboundQueue.createStreamToken(
|
|
782
|
+
accountId,
|
|
783
|
+
pending.sessionExpiresAt,
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
api.logger.info("[soyeht] HTTP pairing handshake completed", { accountId });
|
|
787
|
+
|
|
788
|
+
sendJson(res, 200, {
|
|
789
|
+
ok: true,
|
|
790
|
+
complete: true,
|
|
791
|
+
sessionExpiresAt: pending.sessionExpiresAt,
|
|
792
|
+
streamToken,
|
|
793
|
+
});
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
344
797
|
// ---------------------------------------------------------------------------
|
|
345
798
|
// POST /soyeht/livekit/token — stub
|
|
346
799
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
livekitTokenHandler,
|
|
16
16
|
inboundHandler,
|
|
17
17
|
sseHandler,
|
|
18
|
+
pairingInfoHandler,
|
|
19
|
+
pairingPairHandler,
|
|
20
|
+
pairingFinishHandler,
|
|
18
21
|
} from "./http.js";
|
|
19
22
|
import {
|
|
20
23
|
createSoyehtService,
|
|
@@ -127,6 +130,23 @@ const soyehtPlugin: OpenClawPluginDefinition = {
|
|
|
127
130
|
handler: sseHandler(api, v2deps),
|
|
128
131
|
});
|
|
129
132
|
|
|
133
|
+
// HTTP pairing routes (app pairs via HTTP, no WebSocket needed)
|
|
134
|
+
api.registerHttpRoute({
|
|
135
|
+
path: "/soyeht/pairing/info",
|
|
136
|
+
auth: "plugin",
|
|
137
|
+
handler: pairingInfoHandler(api, v2deps),
|
|
138
|
+
});
|
|
139
|
+
api.registerHttpRoute({
|
|
140
|
+
path: "/soyeht/pairing/pair",
|
|
141
|
+
auth: "plugin",
|
|
142
|
+
handler: pairingPairHandler(api, v2deps),
|
|
143
|
+
});
|
|
144
|
+
api.registerHttpRoute({
|
|
145
|
+
path: "/soyeht/pairing/finish",
|
|
146
|
+
auth: "plugin",
|
|
147
|
+
handler: pairingFinishHandler(api, v2deps),
|
|
148
|
+
});
|
|
149
|
+
|
|
130
150
|
// Background service (manages state lifecycle)
|
|
131
151
|
api.registerService(createSoyehtService(api, v2deps));
|
|
132
152
|
|
package/src/service.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { IdentityBundle, PeerIdentity } from "./identity.js";
|
|
|
15
15
|
import type { RatchetState } from "./ratchet.js";
|
|
16
16
|
import { createOutboundQueue, type OutboundQueue } from "./outbound-queue.js";
|
|
17
17
|
import { renderQrTerminal } from "./qr.js";
|
|
18
|
+
import { resolveSoyehtAccount } from "./config.js";
|
|
18
19
|
|
|
19
20
|
const HEARTBEAT_INTERVAL_MS = 60_000; // 60s
|
|
20
21
|
|
|
@@ -90,14 +91,10 @@ async function showPairingQr(api: OpenClawPluginApi, v2deps: SecurityV2Deps): Pr
|
|
|
90
91
|
const expiresAt = Date.now() + AUTO_PAIRING_TTL_MS;
|
|
91
92
|
const fingerprint = computeFingerprint(identity);
|
|
92
93
|
|
|
93
|
-
// Resolve gatewayUrl from config
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
gatewayUrl = runtime["gatewayUrl"];
|
|
98
|
-
} else if (typeof runtime["baseUrl"] === "string" && runtime["baseUrl"]) {
|
|
99
|
-
gatewayUrl = runtime["baseUrl"];
|
|
100
|
-
}
|
|
94
|
+
// Resolve gatewayUrl from config
|
|
95
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
96
|
+
const account = resolveSoyehtAccount(cfg, accountId);
|
|
97
|
+
const gatewayUrl = account.gatewayUrl;
|
|
101
98
|
|
|
102
99
|
v2deps.pairingSessions.set(pairingToken, {
|
|
103
100
|
token: pairingToken,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PLUGIN_VERSION = "0.2.
|
|
1
|
+
export const PLUGIN_VERSION = "0.2.3";
|