;; ported-from: src/lib/access.ts @ v1.0.0
;;
;; Hardened IPFS access controller: closes an authorship-forgery gap in
;; @orbitdb/core's IPFSAccessController.
;;
;; Upstream, entry verification is split in two: `Entry.verify` checks the
;; signature against `entry.key` (the device signing key), and `canAppend` checks
;; that the identity referenced by `entry.identity` is internally consistent and
;; on the write list. NOTHING binds the two — a room member could craft an entry
;; that references a VICTIM's identity record while being signed with the
;; attacker's own key, and every replica would accept it and attribute it to the
;; victim (fingerprint badge included).
;;
;; This wrapper delegates to the vanilla controller and adds the missing check:
;; the entry's signing key must BE the referenced identity's signing key. Address
;; determinism is preserved — the AC address hashed into the DB manifest is
;; computed by the inner IPFSAccessController from {type:"ipfs", write} only, so
;; wrapped and vanilla clients open the SAME database and interop freely (never
;; change `write`, that would fork it).
;;
;; Caveat: we always open by db NAME, which routes through this factory. An open
;; by raw /orbitdb/... address would rebuild the vanilla controller and silently
;; skip the check — don't add such a path.
(ns ardegazu.rooms.lib.access
(:require ["@orbitdb/core" :as orbit]
[ardegazu.rooms.js :as j]
[shadow.cljs.modern :refer (js-await)]))
(defn signer-matches-identity
"The one check upstream forgot: entry.key must be the identity's own key."
[identities entry]
(let [k (unchecked-get entry "key")
ref (unchecked-get entry "identity")]
(if (or (not (j/truthy? k)) (not (j/truthy? ref)))
(js/Promise.resolve false)
(-> (js/Promise.resolve nil)
(.then (fn [_]
(js-await [ident (.getIdentity ^js identities ref)]
(and (j/truthy? ident)
(identical? (unchecked-get ident "publicKey") k)))))
(.catch (fn [_] false))))))
(defn HardenedIPFSAccessController
"Same call shape as IPFSAccessController({ write }); see the namespace docs."
[options]
(let [base (orbit/IPFSAccessController options)]
(fn [params]
(js-await [ac (base params)]
;; TS `{ ...ac, canAppend }` — own enumerable props copied, canAppend
;; replaced. `type`, `address` and `write` therefore pass through
;; untouched, which is what keeps the manifest address identical.
(let [out (js/Object.assign (js-obj) ac)
identities (unchecked-get params "identities")]
(unchecked-set
out "canAppend"
(fn [entry]
(js-await [ok (.canAppend ^js ac entry)]
(if-not (j/truthy? ok)
false
(signer-matches-identity identities entry)))))
out)))))