@rookdaemon/agora 0.4.0 → 0.4.2

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/dist/index.js CHANGED
@@ -1,27 +1,19 @@
1
1
  import {
2
2
  DEFAULT_BOOTSTRAP_RELAYS,
3
- MessageStore,
4
3
  PeerDiscoveryService,
5
4
  RelayClient,
6
- RelayServer,
7
5
  ReputationStore,
8
- canonicalize,
9
6
  computeAllTrustScores,
10
- computeId,
11
7
  computeTrustScore,
12
8
  computeTrustScores,
13
9
  createCommit,
14
- createEnvelope,
15
10
  createReveal,
16
11
  createVerification,
17
12
  decay,
18
13
  decodeInboundEnvelope,
19
- exportKeyPair,
20
14
  formatDisplayName,
21
- generateKeyPair,
22
15
  getDefaultBootstrapRelay,
23
16
  hashPrediction,
24
- importKeyPair,
25
17
  initPeerConfig,
26
18
  loadPeerConfig,
27
19
  parseBootstrapRelay,
@@ -30,15 +22,33 @@ import {
30
22
  sendToPeer,
31
23
  sendViaRelay,
32
24
  shortKey,
33
- signMessage,
34
25
  validateCommitRecord,
35
26
  validateRevealRecord,
36
27
  validateVerificationRecord,
37
- verifyEnvelope,
38
28
  verifyReveal,
39
- verifySignature,
40
29
  verifyVerificationSignature
41
- } from "./chunk-7RX2YC4A.js";
30
+ } from "./chunk-IOHECZYT.js";
31
+ import {
32
+ MessageBuffer,
33
+ createRestRouter,
34
+ createToken,
35
+ requireAuth,
36
+ revokeToken,
37
+ runRelay
38
+ } from "./chunk-2U4PZINT.js";
39
+ import {
40
+ MessageStore,
41
+ RelayServer,
42
+ canonicalize,
43
+ computeId,
44
+ createEnvelope,
45
+ exportKeyPair,
46
+ generateKeyPair,
47
+ importKeyPair,
48
+ signMessage,
49
+ verifyEnvelope,
50
+ verifySignature
51
+ } from "./chunk-D7Y66GFC.js";
42
52
 
43
53
  // src/registry/capability.ts
44
54
  import { createHash } from "crypto";
@@ -495,426 +505,22 @@ async function loadAgoraConfigAsync(path) {
495
505
  return parseConfig(config);
496
506
  }
497
507
 
498
- // src/relay/message-buffer.ts
499
- var MAX_MESSAGES_PER_AGENT = 100;
500
- var MessageBuffer = class {
501
- buffers = /* @__PURE__ */ new Map();
502
- /**
503
- * Add a message to an agent's buffer.
504
- * Evicts the oldest message if the buffer is full.
505
- */
506
- add(publicKey, message) {
507
- let queue = this.buffers.get(publicKey);
508
- if (!queue) {
509
- queue = [];
510
- this.buffers.set(publicKey, queue);
511
- }
512
- queue.push(message);
513
- if (queue.length > MAX_MESSAGES_PER_AGENT) {
514
- queue.shift();
515
- }
516
- }
517
- /**
518
- * Retrieve messages for an agent, optionally filtering by `since` timestamp.
519
- * Returns messages with timestamp > since (exclusive).
520
- */
521
- get(publicKey, since) {
522
- const queue = this.buffers.get(publicKey) ?? [];
523
- if (since === void 0) {
524
- return [...queue];
525
- }
526
- return queue.filter((m) => m.timestamp > since);
527
- }
528
- /**
529
- * Clear all messages for an agent (after polling without `since`).
530
- */
531
- clear(publicKey) {
532
- this.buffers.set(publicKey, []);
533
- }
534
- /**
535
- * Remove all state for a disconnected agent.
536
- */
537
- delete(publicKey) {
538
- this.buffers.delete(publicKey);
539
- }
540
- };
541
-
542
- // src/relay/jwt-auth.ts
543
- import jwt from "jsonwebtoken";
544
- import { randomBytes } from "crypto";
545
- var revokedJtis = /* @__PURE__ */ new Map();
546
- function pruneExpiredRevocations() {
547
- const now = Date.now();
548
- for (const [jti, expiry] of revokedJtis) {
549
- if (expiry <= now) {
550
- revokedJtis.delete(jti);
551
- }
552
- }
553
- }
554
- function getJwtSecret() {
555
- const secret = process.env.AGORA_RELAY_JWT_SECRET;
556
- if (!secret) {
557
- throw new Error(
558
- "AGORA_RELAY_JWT_SECRET environment variable is required but not set"
559
- );
560
- }
561
- return secret;
562
- }
563
- function getExpirySeconds() {
564
- const raw = process.env.AGORA_JWT_EXPIRY_SECONDS;
565
- if (raw) {
566
- const parsed = parseInt(raw, 10);
567
- if (!isNaN(parsed) && parsed > 0) {
568
- return parsed;
569
- }
570
- }
571
- return 3600;
572
- }
573
- function createToken(payload) {
574
- const secret = getJwtSecret();
575
- const expirySeconds = getExpirySeconds();
576
- const jti = `${Date.now()}-${randomBytes(16).toString("hex")}`;
577
- const token = jwt.sign(
578
- { publicKey: payload.publicKey, name: payload.name, jti },
579
- secret,
580
- { expiresIn: expirySeconds }
581
- );
582
- const expiresAt = Date.now() + expirySeconds * 1e3;
583
- return { token, expiresAt };
584
- }
585
- function revokeToken(token) {
586
- try {
587
- const secret = getJwtSecret();
588
- const decoded = jwt.verify(token, secret);
589
- if (decoded.jti) {
590
- const expiry = decoded.exp ? decoded.exp * 1e3 : Date.now();
591
- revokedJtis.set(decoded.jti, expiry);
592
- pruneExpiredRevocations();
593
- }
594
- } catch {
595
- }
596
- }
597
- function requireAuth(req, res, next) {
598
- const authHeader = req.headers.authorization;
599
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
600
- res.status(401).json({ error: "Missing or malformed Authorization header" });
601
- return;
602
- }
603
- const token = authHeader.slice(7);
604
- try {
605
- const secret = getJwtSecret();
606
- const decoded = jwt.verify(token, secret);
607
- if (decoded.jti && revokedJtis.has(decoded.jti)) {
608
- res.status(401).json({ error: "Token has been revoked" });
609
- return;
610
- }
611
- req.agent = { publicKey: decoded.publicKey, name: decoded.name };
612
- next();
613
- } catch (err) {
614
- if (err instanceof jwt.TokenExpiredError) {
615
- res.status(401).json({ error: "Token expired" });
616
- } else {
617
- res.status(401).json({ error: "Invalid token" });
618
- }
619
- }
620
- }
621
-
622
- // src/relay/rest-api.ts
623
- import { Router } from "express";
624
- import { rateLimit } from "express-rate-limit";
625
- var apiRateLimit = rateLimit({
626
- windowMs: 6e4,
627
- limit: 60,
628
- standardHeaders: "draft-7",
629
- legacyHeaders: false,
630
- message: { error: "Too many requests \u2014 try again later" }
631
- });
632
- function pruneExpiredSessions(sessions, buffer) {
633
- const now = Date.now();
634
- for (const [publicKey, session] of sessions) {
635
- if (session.expiresAt <= now) {
636
- sessions.delete(publicKey);
637
- buffer.delete(publicKey);
638
- }
639
- }
640
- }
641
- function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv) {
642
- const router = Router();
643
- router.use(apiRateLimit);
644
- relay.on("message-relayed", (from, to, envelope) => {
645
- if (!sessions.has(to)) return;
646
- const agentMap = relay.getAgents();
647
- const senderAgent = agentMap.get(from);
648
- const env = envelope;
649
- const msg = {
650
- id: env.id,
651
- from,
652
- fromName: senderAgent?.name,
653
- type: env.type,
654
- payload: env.payload,
655
- timestamp: env.timestamp,
656
- inReplyTo: env.inReplyTo
657
- };
658
- buffer.add(to, msg);
659
- });
660
- router.post("/v1/register", async (req, res) => {
661
- const { publicKey, privateKey, name, metadata } = req.body;
662
- if (!publicKey || typeof publicKey !== "string") {
663
- res.status(400).json({ error: "publicKey is required" });
664
- return;
665
- }
666
- if (!privateKey || typeof privateKey !== "string") {
667
- res.status(400).json({ error: "privateKey is required" });
668
- return;
669
- }
670
- const testEnvelope = createEnv(
671
- "announce",
672
- publicKey,
673
- privateKey,
674
- { challenge: "register" },
675
- Date.now()
676
- );
677
- const verification = verifyEnv(testEnvelope);
678
- if (!verification.valid) {
679
- res.status(400).json({ error: "Key pair verification failed: " + verification.reason });
680
- return;
681
- }
682
- const { token, expiresAt } = createToken({ publicKey, name });
683
- pruneExpiredSessions(sessions, buffer);
684
- const session = {
685
- publicKey,
686
- privateKey,
687
- name,
688
- metadata,
689
- registeredAt: Date.now(),
690
- expiresAt,
691
- token
692
- };
693
- sessions.set(publicKey, session);
694
- const wsAgents = relay.getAgents();
695
- const peers = [];
696
- for (const agent of wsAgents.values()) {
697
- if (agent.publicKey !== publicKey) {
698
- peers.push({
699
- publicKey: agent.publicKey,
700
- name: agent.name,
701
- lastSeen: agent.lastSeen
702
- });
703
- }
704
- }
705
- for (const s of sessions.values()) {
706
- if (s.publicKey !== publicKey && !wsAgents.has(s.publicKey)) {
707
- peers.push({
708
- publicKey: s.publicKey,
709
- name: s.name,
710
- lastSeen: s.registeredAt
711
- });
712
- }
713
- }
714
- res.json({ token, expiresAt, peers });
715
- });
716
- router.post(
717
- "/v1/send",
718
- requireAuth,
719
- async (req, res) => {
720
- const { to, type, payload, inReplyTo } = req.body;
721
- if (!to || typeof to !== "string") {
722
- res.status(400).json({ error: "to is required" });
723
- return;
724
- }
725
- if (!type || typeof type !== "string") {
726
- res.status(400).json({ error: "type is required" });
727
- return;
728
- }
729
- if (payload === void 0) {
730
- res.status(400).json({ error: "payload is required" });
731
- return;
732
- }
733
- const senderPublicKey = req.agent.publicKey;
734
- const session = sessions.get(senderPublicKey);
735
- if (!session) {
736
- res.status(401).json({ error: "Session not found \u2014 please re-register" });
737
- return;
738
- }
739
- const envelope = createEnv(
740
- type,
741
- senderPublicKey,
742
- session.privateKey,
743
- payload,
744
- Date.now(),
745
- inReplyTo
746
- );
747
- const wsAgents = relay.getAgents();
748
- const wsRecipient = wsAgents.get(to);
749
- if (wsRecipient && wsRecipient.socket) {
750
- const ws = wsRecipient.socket;
751
- const OPEN = 1;
752
- if (ws.readyState !== OPEN) {
753
- res.status(503).json({ error: "Recipient connection is not open" });
754
- return;
755
- }
756
- try {
757
- const relayMsg = JSON.stringify({
758
- type: "message",
759
- from: senderPublicKey,
760
- name: session.name,
761
- envelope
762
- });
763
- ws.send(relayMsg);
764
- res.json({ ok: true, envelopeId: envelope.id });
765
- return;
766
- } catch (err) {
767
- res.status(500).json({
768
- error: "Failed to deliver message: " + (err instanceof Error ? err.message : String(err))
769
- });
770
- return;
771
- }
772
- }
773
- const restRecipient = sessions.get(to);
774
- if (restRecipient) {
775
- const senderAgent = wsAgents.get(senderPublicKey);
776
- const msg = {
777
- id: envelope.id,
778
- from: senderPublicKey,
779
- fromName: session.name ?? senderAgent?.name,
780
- type: envelope.type,
781
- payload: envelope.payload,
782
- timestamp: envelope.timestamp,
783
- inReplyTo: envelope.inReplyTo
784
- };
785
- buffer.add(to, msg);
786
- res.json({ ok: true, envelopeId: envelope.id });
787
- return;
788
- }
789
- res.status(404).json({ error: "Recipient not connected" });
790
- }
791
- );
792
- router.get(
793
- "/v1/peers",
794
- requireAuth,
795
- (req, res) => {
796
- const callerPublicKey = req.agent.publicKey;
797
- const wsAgents = relay.getAgents();
798
- const peerList = [];
799
- for (const agent of wsAgents.values()) {
800
- if (agent.publicKey !== callerPublicKey) {
801
- peerList.push({
802
- publicKey: agent.publicKey,
803
- name: agent.name,
804
- lastSeen: agent.lastSeen,
805
- metadata: agent.metadata
806
- });
807
- }
808
- }
809
- for (const s of sessions.values()) {
810
- if (s.publicKey !== callerPublicKey && !wsAgents.has(s.publicKey)) {
811
- peerList.push({
812
- publicKey: s.publicKey,
813
- name: s.name,
814
- lastSeen: s.registeredAt,
815
- metadata: s.metadata
816
- });
817
- }
818
- }
819
- res.json({ peers: peerList });
820
- }
821
- );
822
- router.get(
823
- "/v1/messages",
824
- requireAuth,
825
- (req, res) => {
826
- const publicKey = req.agent.publicKey;
827
- const sinceRaw = req.query.since;
828
- const limitRaw = req.query.limit;
829
- const since = sinceRaw ? parseInt(sinceRaw, 10) : void 0;
830
- const limit = Math.min(limitRaw ? parseInt(limitRaw, 10) : 50, 100);
831
- let messages = buffer.get(publicKey, since);
832
- const hasMore = messages.length > limit;
833
- if (hasMore) {
834
- messages = messages.slice(0, limit);
835
- }
836
- if (since === void 0) {
837
- buffer.clear(publicKey);
838
- }
839
- res.json({ messages, hasMore });
840
- }
841
- );
842
- router.delete(
843
- "/v1/disconnect",
844
- requireAuth,
845
- (req, res) => {
846
- const publicKey = req.agent.publicKey;
847
- const authHeader = req.headers.authorization;
848
- const token = authHeader.slice(7);
849
- revokeToken(token);
850
- sessions.delete(publicKey);
851
- buffer.delete(publicKey);
852
- res.json({ ok: true });
853
- }
854
- );
855
- return router;
856
- }
857
-
858
- // src/relay/run-relay.ts
859
- import http from "http";
860
- import express from "express";
861
- var createEnvelopeForRest = (type, sender, privateKey, payload, timestamp, inReplyTo) => createEnvelope(
862
- type,
863
- sender,
864
- privateKey,
865
- payload,
866
- timestamp ?? Date.now(),
867
- inReplyTo
868
- );
869
- async function runRelay(options = {}) {
870
- const wsPort = options.wsPort ?? parseInt(process.env.PORT ?? "3001", 10);
871
- const enableRest = options.enableRest ?? (typeof process.env.AGORA_RELAY_JWT_SECRET === "string" && process.env.AGORA_RELAY_JWT_SECRET.length > 0);
872
- const relay = new RelayServer(options.relayOptions);
873
- await relay.start(wsPort);
874
- if (!enableRest) {
875
- return { relay };
876
- }
877
- if (!process.env.AGORA_RELAY_JWT_SECRET) {
878
- await relay.stop();
879
- throw new Error(
880
- "AGORA_RELAY_JWT_SECRET environment variable is required when REST API is enabled"
881
- );
882
- }
883
- const restPort = options.restPort ?? wsPort + 1;
884
- const messageBuffer = new MessageBuffer();
885
- const restSessions = /* @__PURE__ */ new Map();
886
- const app = express();
887
- app.use(express.json());
888
- const verifyForRest = (envelope) => verifyEnvelope(envelope);
889
- const router = createRestRouter(
890
- relay,
891
- messageBuffer,
892
- restSessions,
893
- createEnvelopeForRest,
894
- verifyForRest
895
- );
896
- app.use(router);
897
- app.use((_req, res) => {
898
- res.status(404).json({ error: "Not found" });
899
- });
900
- const httpServer = http.createServer(app);
901
- await new Promise((resolve2, reject) => {
902
- httpServer.listen(restPort, () => resolve2());
903
- httpServer.on("error", reject);
904
- });
905
- return { relay, httpServer };
906
- }
907
-
908
508
  // src/service.ts
909
509
  var AgoraService = class {
910
510
  config;
911
511
  relayClient = null;
912
- relayMessageHandler = null;
913
- relayMessageHandlerWithName = null;
512
+ onRelayMessage;
914
513
  logger;
915
514
  relayClientFactory;
916
- constructor(config, logger, relayClientFactory) {
515
+ /**
516
+ * @param config - Service config (identity, peers, optional relay)
517
+ * @param onRelayMessage - Required callback for relay messages. Ensures no messages are lost between init and connect.
518
+ * @param logger - Optional debug logger
519
+ * @param relayClientFactory - Optional factory for relay client (for testing)
520
+ */
521
+ constructor(config, onRelayMessage, logger, relayClientFactory) {
917
522
  this.config = config;
523
+ this.onRelayMessage = onRelayMessage;
918
524
  this.logger = logger ?? null;
919
525
  this.relayClientFactory = relayClientFactory ?? null;
920
526
  }
@@ -931,7 +537,7 @@ var AgoraService = class {
931
537
  error: `Unknown peer: ${options.peerName}`
932
538
  };
933
539
  }
934
- if (peer.url) {
540
+ if (peer.url && !options.relayOnly) {
935
541
  const transportConfig = {
936
542
  identity: {
937
543
  publicKey: this.config.identity.publicKey,
@@ -950,6 +556,19 @@ var AgoraService = class {
950
556
  return httpResult;
951
557
  }
952
558
  this.logger?.debug(`HTTP send to ${options.peerName} failed: ${httpResult.error}`);
559
+ if (options.direct) {
560
+ return {
561
+ ok: false,
562
+ status: httpResult.status,
563
+ error: `Direct send to ${options.peerName} failed: ${httpResult.error}`
564
+ };
565
+ }
566
+ } else if (options.direct && !peer.url) {
567
+ return {
568
+ ok: false,
569
+ status: 0,
570
+ error: `Direct send failed: peer '${options.peerName}' has no URL configured`
571
+ };
953
572
  }
954
573
  if (this.relayClient?.connected() && this.config.relay) {
955
574
  const relayResult = await sendViaRelay(
@@ -1024,11 +643,7 @@ var AgoraService = class {
1024
643
  this.logger?.debug(`Agora relay error: ${error.message}`);
1025
644
  });
1026
645
  this.relayClient.on("message", (envelope, from, fromName) => {
1027
- if (this.relayMessageHandlerWithName) {
1028
- this.relayMessageHandlerWithName(envelope, from, fromName);
1029
- } else if (this.relayMessageHandler) {
1030
- this.relayMessageHandler(envelope);
1031
- }
646
+ this.onRelayMessage(envelope, from, fromName);
1032
647
  });
1033
648
  try {
1034
649
  await this.relayClient.connect();
@@ -1038,14 +653,6 @@ var AgoraService = class {
1038
653
  this.relayClient = null;
1039
654
  }
1040
655
  }
1041
- setRelayMessageHandler(handler) {
1042
- this.relayMessageHandler = handler;
1043
- this.relayMessageHandlerWithName = null;
1044
- }
1045
- setRelayMessageHandlerWithName(handler) {
1046
- this.relayMessageHandlerWithName = handler;
1047
- this.relayMessageHandler = null;
1048
- }
1049
656
  async disconnectRelay() {
1050
657
  if (this.relayClient) {
1051
658
  this.relayClient.disconnect();