;; 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")))))))