package utils import ( "crypto/hmac" "encoding/base64" "encoding/json" "fmt" "strings" "time" ) // The approval card. // // Nearle Buddy never writes anything. When a question would change something, // the assistant RESOLVES what would happen and hands back a card; a person reads // it and presses approve; the server then performs the write itself. The model // is not in that second half at all. // // ── Why the card is signed rather than stored ─────────────────────────────── // // The resolved action cannot be kept on the client, or the thing approved would // be whatever the browser sent back. It could be kept on the server in a table // or in Redis — but a pending approval lives for about a minute, and a signed // card needs no storage, no expiry sweep, and no shared state between pods. The // signature is what makes it trustworthy, exactly as with the session token next // door, and with the same key. // // So a card says: this user, in this shop, approved this exact action, and here // is the proof it was this server that resolved it. // // ── Replay ───────────────────────────────────────────────────────────────── // // A signed card carries no nonce, so nothing here stops it being submitted // twice. That is deliberate and is handled where it belongs: every write // re-validates against the live database before it runs. Approving the same // stock request twice finds it already approved the second time and refuses. // A single-use token would put that guarantee in the wrong place — the state a // write depends on can change between resolving and approving anyway, so the // check has to happen at execution whether or not a card can be replayed. type Card struct { // The tool that resolved this, and the arguments it resolved to. Not the // arguments the MODEL sent: resolved ones, after defaults and validation. Tool string `json:"t"` Args map[string]any `json:"a"` // Who may approve it. A card is not transferable — the session presenting // it must be the session it was issued to, or one person's approval could // be replayed by another. Userid int `json:"uid"` Tenantid int `json:"tid"` // Short. A card is read and pressed within a minute or abandoned; an hour // would mean approving something resolved against a shop that has moved on. Expiresat int64 `json:"exp"` } // CardTTL is how long a resolved action stays approvable. const CardTTL = 5 * time.Minute const cardPrefix = "c1." // MintCard signs a resolved action. // // Shares the session signing key. One key for the deployment, one place it can // be missing — and the prefix is what stops a card verifying as a session token // or the other way round. func MintCard(card Card, now time.Time) (string, error) { secret, err := posTokenSecret() if err != nil { return "", err } card.Expiresat = now.Add(CardTTL).Unix() payload, err := json.Marshal(card) if err != nil { return "", err } encoded := base64.RawURLEncoding.EncodeToString(payload) return cardPrefix + encoded + "." + sign(encoded, secret), nil } // ParseCard verifies a card and returns what it authorises. // // The signature is checked before anything in the payload is believed — // including the expiry, and including whose card it is. Reading `uid` out of an // unverified payload would be taking the caller's word for whose approval this // was. func ParseCard(raw string, now time.Time) (Card, error) { secret, err := posTokenSecret() if err != nil { return Card{}, err } after, found := strings.CutPrefix(strings.TrimSpace(raw), cardPrefix) if !found { return Card{}, fmt.Errorf("not an approval card") } encoded, signature, found := strings.Cut(after, ".") if !found || encoded == "" || signature == "" { return Card{}, fmt.Errorf("malformed approval card") } if !hmac.Equal([]byte(signature), []byte(sign(encoded, secret))) { return Card{}, fmt.Errorf("this approval was not issued by this server") } payload, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { return Card{}, fmt.Errorf("malformed approval card") } var card Card if err := json.Unmarshal(payload, &card); err != nil { return Card{}, fmt.Errorf("malformed approval card") } if card.Expiresat > 0 && now.Unix() >= card.Expiresat { return Card{}, fmt.Errorf("this approval has expired; ask again to get a fresh one") } if card.Tool == "" || card.Userid <= 0 { return Card{}, fmt.Errorf("this approval names no action") } return card, nil }