;; The `#pay=` deep link: a signed payment request as a URL.
;;
;; https://banca.ardegazu.ro/#pay=<b64url(JSON.stringify(wpr))>
;;
;; Fragment-only, like every capability link in this suite: the request never
;; reaches a server, not banca's and not anyone's, because everything after the
;; `#` stays in the browser. A payee generates one and hands it over by whatever
;; means they already have — a chat message, a QR code, a piece of paper.
;;
;; Nothing here is a capability to SPEND. A request is a claim about what
;; someone wants and who they are, and it is worth exactly its signature: the
;; parse verifies before it returns, and returns null otherwise. Deciding to pay
;; is a separate act, and it produces a separate signed artifact.
;;
;; Its own module (dist/paylink.js) so a page that only shows "someone is asking
;; you for 12.00" does not pull LedgerStore and a localStorage-backed fold in
;; with it — the same reason social-kit's `./join` chip is not in its index.
(ns ardegazu.wallet.paylink
(:require [ardegazu.id.crypto :as id-crypto]
[ardegazu.wallet.consts :as consts]
[ardegazu.wallet.shapes :as shapes])
(:require-macros [ardegazu.wallet.macros :refer [oget]]))
(def ^:private td (js/TextDecoder.))
;; Canonical base64url and nothing else — the same discipline the signature and
;; digest shapes keep in consts.cljs, applied to the one place a request arrives
;; as a STRING that something might key on. Standard base64 (`+` `/` `=`) and
;; percent-encoding both decode to the same request under a lenient decoder,
;; which would make "the same link" a question with three answers for any cache,
;; dedup or "have I already shown this?" check a wallet builds on top. Nothing
;; honest emits them: base64url has no character a URL fragment escapes, so a
;; percent sequence in a `pay=` value is a re-encoder's fingerprint, not a user's
;; link. A length ≡ 1 (mod 4) cannot be the encoding of any byte string.
(def ^:private B64URL-RE #"^[A-Za-z0-9_-]+$")
(defn- b64url? [s]
(and (string? s)
(.test B64URL-RE s)
(not (identical? 1 (mod (.-length s) 4)))))
(defn pay-request-url
"A verified-on-arrival link for one payment request.
BUILD-SIDE, so it REJECTS rather than returning null: `payRequestUrl(null)`
used to hand back a perfectly well-formed link to the four bytes `null`, and a
caller who got a string back had no way to learn otherwise until a stranger
opened it. The request is re-shaped and the REBUILT one is what gets encoded,
so a link always carries the documented key order."
[wpr]
;; the link builder's own wall clock: a link is minted in the present
(let [r (shapes/pay-request-shape wpr js/undefined)]
(when (nil? r)
(throw (js/Error. "wallet-kit: payRequestUrl needs a structurally valid wpr")))
(str consts/PAY-BASE-URL "#" consts/PAY-FRAGMENT-KEY "="
(id-crypto/to-b64url (id-crypto/utf8 (js/JSON.stringify r))))))
(defn- fragment-of
"Everything after the first `#`, or the input itself when there is none — so a
caller may pass `location.hash`, a bare fragment, or a whole URL."
[s]
(if-not (string? s)
""
(let [i (.indexOf s "#")]
(if (< i 0) s (.slice s (inc i))))))
(defn- pay-param
"The `pay=` value out of a fragment that may carry other parameters. base64url
has no `&` in its alphabet, so splitting on `&` can never cut a payload in
half."
[frag]
(let [prefix (str consts/PAY-FRAGMENT-KEY "=")
parts (.split frag "&")
found (js-obj "v" nil)]
(dotimes [i (.-length ^js parts)]
(let [p (aget parts i)]
(when (and (nil? (oget found "v")) (identical? 0 (.indexOf p prefix)))
(unchecked-set found "v" (.slice p (.-length prefix))))))
(oget found "v")))
(defn parse-pay-fragment
"Decode + fully verify a `#pay=` fragment. null on ANY failure — no such
parameter, a non-canonical or malformed encoding, bad JSON, or a request whose
signature does not hold — and never throws."
[fragment]
(-> (js/Promise.resolve nil)
(.then (fn [_]
(let [v (pay-param (fragment-of fragment))]
(if-not (b64url? v)
nil
;; a link is read by a person, now: the reader's wall clock is
;; the right admission clock, and there is no fold here to
;; make deterministic
(shapes/verify-pay-request
(js/JSON.parse (.decode td (id-crypto/from-b64url v))) js/undefined)))))
(.catch (fn [_] nil))))