peer-kit / src / ardegazu / peer / core / net.cljs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
;; ported-from: src/core/index.ts @ v1.3.0
;;
;; Assembles the p2p layer: relay bootstrap → libp2p node → peer manager,
;; exposing the same small surface the suite's games consume — minus the
;; browser wake listeners (call net.wake() from your own lifecycle instead).
;;
;; Vendor retirement: `clamp-profile` and `verify-assertion` now come from the
;; real ardegazu-id-kit instead of src/vendor/id — compiled from its CLJS
;; sources on this build's classpath (see deps.edn), the way every ardegazu
;; kit is consumed. Only foreign packages stay string requires.
(ns ardegazu.peer.core.net
  (:require [ardegazu.id.identity :as id-identity]
            [ardegazu.id.profile :as id-profile]
            [ardegazu.peer.obj :as obj]
            [ardegazu.peer.core.bootstrap :as bootstrap]
            [ardegazu.peer.core.room-crypto :as rc]
            [ardegazu.peer.core.turn :as turn]
            [ardegazu.peer.core.node :as netnode]
            [ardegazu.peer.core.peers :as peers]
            [shadow.cljs.modern :refer (js-await)]))

(def ^:private ROSTER-GRACE-MS 4000)

(defn- build-hello-identity
  "My identity block for sealed hellos: one session binding, precomputed —
   plus the verifier for remote ones, kept as injected callbacks so
   core/peers stays crypto-agnostic."
  [ident app-salt room-id my-peer-id]
  (-> (js/Promise.resolve nil)
      (.then
       (fn [_]
         (-> ((unchecked-get ident "sign") room-id my-peer-id)
             (.then
              (fn [sig]
                ;; `{pub, sig, ...(name?), ...(hue?), ...(glyph?)}` — the exact
                ;; conditional-spread order of the TS original, built with
                ;; sequential sets so the key order on the wire is fixed
                (let [mine (js-obj)
                      nm (unchecked-get ident "name")
                      hue (unchecked-get ident "hue")
                      glyph (unchecked-get ident "glyph")]
                  (unchecked-set mine "pub" (unchecked-get ident "pub"))
                  (unchecked-set mine "sig" sig)
                  (when ^boolean (js* "!!(~{})" nm) (unchecked-set mine "name" nm))
                  (when (identical? "number" (js* "typeof ~{}" hue)) (unchecked-set mine "hue" hue))
                  (when ^boolean (js* "!!(~{})" glyph) (unchecked-set mine "glyph" glyph))
                  mine)))
             ;; signing unavailable — announce nothing
             (.catch (fn [_] nil)))))
      (.then
       (fn [mine]
         (js-obj
          "mine" mine
          "verify"
          (fn [peer-id raw]
            (-> (js/Promise.resolve nil)
                (.then
                 (fn [_]
                   (if-not (and (some? raw)
                                (identical? "object" (js* "typeof ~{}" raw))
                                (string? (unchecked-get raw "pub"))
                                (string? (unchecked-get raw "sig")))
                     nil
                     (js-await [fp (id-identity/verify-assertion app-salt room-id peer-id
                                                                (unchecked-get raw "pub")
                                                                (unchecked-get raw "sig"))]
                       (if-not ^boolean (js* "!!(~{})" fp)
                         nil ; forged binding — treat the peer as identity-less
                         (let [prof (id-profile/clamp-profile
                                     (js-obj "name" (unchecked-get raw "name")
                                             "hue" (unchecked-get raw "hue")
                                             "glyph" (unchecked-get raw "glyph")))
                               out (js-obj "pub" (unchecked-get raw "pub"))]
                           (when ^boolean (js* "!!(~{})" (unchecked-get prof "name"))
                             (unchecked-set out "name" (unchecked-get prof "name")))
                           (when-not (nil? (unchecked-get prof "hue"))
                             (unchecked-set out "hue" (unchecked-get prof "hue")))
                           (when-not (nil? (unchecked-get prof "glyph"))
                             (unchecked-set out "glyph" (unchecked-get prof "glyph")))
                           out)))))))))))))

(defn create-net
  "cfg = #js {:secret :appSalt :protocolPrefix :descriptorUrl :turnUrl
              :relayFallback :identity :webrtc}
   cb  = #js {:onRoomRoster :onPeerOpen :onPeerGone :onMessage :onStatus
              :onPeerIdentity?}
   Resolves to the Net surface (see types/index.d.ts)."
  [cfg cb]
  (-> (js/Promise.resolve nil)
      (.then
       (fn [_]
         (js-await [room-crypto (rc/create (unchecked-get cfg "secret")
                                           (unchecked-get cfg "appSalt"))]
           (js-await [relay (bootstrap/fetch-relay-info (unchecked-get cfg "descriptorUrl")
                                                        (unchecked-get cfg "relayFallback"))]
             (let [ice (turn/IceConfig. (unchecked-get cfg "turnUrl") js/undefined)]
               (js-await [nn (netnode/create relay ice
                                             (js-obj "webrtc" (unchecked-get cfg "webrtc")))]
                 (let [^js node (unchecked-get nn "node")
                       my-id (.toString (.-peerId node))
                       cfg-ident (unchecked-get cfg "identity")]
                   (js-await [hello-id (if ^boolean (js* "!!(~{})" cfg-ident)
                                         (build-hello-identity cfg-ident
                                                               (unchecked-get cfg "appSalt")
                                                               (unchecked-get room-crypto "roomId")
                                                               my-id)
                                         (js/Promise.resolve nil))]
                     (let [call (fn [k & args]
                                  (let [f (unchecked-get cb k)]
                                    (when ^boolean (js* "!!(~{})" f) (apply f args))
                                    js/undefined))
                           pm (peers/PeerManager.
                               node room-crypto (unchecked-get relay "peerId")
                               (unchecked-get cfg "protocolPrefix")
                               (js-obj
                                "peerOpen" (fn [pid] (call "onPeerOpen" pid) (call "onStatus"))
                                "peerGone" (fn [pid] (call "onPeerGone" pid) (call "onStatus"))
                                "message" (fn [from payload] (call "onMessage" from payload))
                                "membersChanged" (fn [ms] (call "onRoomRoster" ms) (call "onStatus"))
                                "peerIdentity" (fn [pid ident] (call "onPeerIdentity" pid ident)))
                               hello-id)]
                       (js-await [_ (peers/start pm)]
                         (do
                           ;; solo boot: nobody appeared within the grace window
                           ;; ⇒ tell the app the room is empty so it can claim
                           ;; hostship (works fully offline too)
                           (js/setTimeout
                            (fn []
                              (when (zero? (.-length (peers/members pm)))
                                (call "onRoomRoster" (array))))
                            ROSTER-GRACE-MS)

                           (.addEventListener node "peer:connect" (fn [] (call "onStatus")))
                           (.addEventListener node "peer:disconnect" (fn [] (call "onStatus")))

                           ;; 13 keys — past the eight-pair js-obj limit, so
                           ;; built with sequential sets (ardegazu.peer.obj)
                           (obj/ordered
                            "myId" my-id
                            "roomPeers" (fn [] (js/Set. (peers/members pm)))
                            "relayConnected" (fn [] (netnode/relay-connected? nn))
                            "openPeers" (fn [] (peers/open-peers pm))
                            "openCount" (fn [] (peers/open-count pm))
                            "send" (fn [to frame] (peers/send-to pm to frame))
                            "broadcast" (fn [frame] (peers/broadcast pm frame))
                            "identityOf" (fn [pid] (peers/identity-of pm pid))
                            "libp2p" node
                            "roomId" (unchecked-get room-crypto "roomId")
                            "wake" (fn []
                                     (.then (netnode/redial-relay nn) (fn [_] (call "onStatus")))
                                     (peers/wake pm)
                                     js/undefined)
                            "close" (fn [] (.stop node))))))))))))))))

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/peer-kit.git