id-kit / src / ardegazu / id / protocol.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
;; ported-from: src/protocol.ts @ v1.1.0
;;
;; The identity-bridge wire contract: message guards, the apex storage key,
;; the seed shape, and the origin allowlist. Every message is versioned
;; (`v: 1`) and requests echo a `reqId`. Seeds only ever travel in replies
;; sent via `event.source.postMessage(msg, event.origin)` after the origin
;; gate passed — never with targetOrigin "*".
(ns ardegazu.id.protocol)

;; Where the bridge page lives, relative to the apex origin.
(def BRIDGE-PATH "/id/")
(def DEFAULT-BRIDGE-URL (str "https://ardegazu.ro" BRIDGE-PATH))

;; The one localStorage key on the apex origin.
(def BRIDGE-STORE-KEY "ardegazu-id:v1")

;; 32 random bytes, base64url — the whole identity.
(def SEED-RE #"^[A-Za-z0-9_-]{43}$")

;; Origins allowed to talk to the bridge host.
;; (the TS source spells this ^https:\/\/… — '/' needs no escape in a JS
;; regex; the compiled automaton is identical)
(def APP-ORIGIN-RE #"^https://([a-z0-9-]+\.)?ardegazu\.ro$")
(def DEV-ORIGINS
  #js ["http://localhost:4173"
       "http://localhost:5173"
       "http://127.0.0.1:4173"
       "http://127.0.0.1:5173"])

(defn is-allowed-app-origin [origin]
  (or (.test APP-ORIGIN-RE origin)
      (.includes ^js DEV-ORIGINS origin)))

;; ---- app sections ----------------------------------------------------------
;;
;; The record's `apps` map is write-isolated per app and readable by all: an
;; app may only ever write the entry named by ITS OWN origin. The key is a
;; pure function of the origin string, so a caller cannot name someone else's
;; section — never derive it from anything an app claims.
(def APP-KEY-RE #"^[a-z0-9-]{1,32}$")

;; Keys the suite hands out by construction, not by subdomain label: "home" is
;; the apex hub and "dev" is every localhost dev origin. A subdomain must never
;; derive either of them — deploying https://home.ardegazu.ro or
;; https://dev.ardegazu.ro would otherwise silently hand that host write access
;; to a section owned by someone else. Neither host exists; this keeps it that
;; way. (Reserving them here also means the label is simply unusable, rather
;; than usable-until-someone-deploys-it.)
(def RESERVED-APP-KEYS #js ["home" "dev"])

(defn app-key-of-origin
  "The `apps` key an origin owns: the subdomain label for https://<label>.ardegazu.ro,
   \"home\" for the apex, \"dev\" for any dev origin, nil for anything else.
   The labels \"home\" and \"dev\" are RESERVED — a subdomain spelling either of
   them derives nil, not the reserved key."
  [origin]
  (if-not (string? origin)
    nil
    (if (.includes ^js DEV-ORIGINS origin)
      "dev"
      (let [m (.exec APP-ORIGIN-RE origin)
            ;; group 1 is "<label>." on a subdomain, undefined on the apex
            label (if (some? m) (aget m 1) nil)]
        (cond
          (nil? m) nil
          (nil? label) "home"
          :else (let [k (.slice label 0 (dec (.-length label)))]
                  (if (and (.test APP-KEY-RE k)
                           (not (.includes ^js RESERVED-APP-KEYS k)))
                    k
                    nil)))))))

(defn is-bridge-reply [x]
  (boolean
   (and x
        (identical? 1 (unchecked-get x "v"))
        (let [t (unchecked-get x "t")]
          (or (identical? t "id-ready")
              (identical? t "state")
              (identical? t "conflict"))))))

(defn is-bridge-request [x]
  (boolean
   (and x
        (identical? 1 (unchecked-get x "v"))
        (number? (unchecked-get x "reqId"))
        (let [t (unchecked-get x "t")]
          (or (identical? t "get")
              (identical? t "put")
              (identical? t "profile")
              (identical? t "soc")
              (identical? t "clear")
              ;; the app-section write carries its own payload guard: a CAS
              ;; seed and a `state` key that must be PRESENT (any JSON value,
              ;; null included — null is the delete). OWN property only: `in`
              ;; walks the prototype chain, so a stray Object.prototype.state
              ;; would make every `app` message look well-formed.
              (and (identical? t "app")
                   (string? (unchecked-get x "expect"))
                   (js/Object.hasOwn x "state")))))))

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