| 1 | (() => { |
| 2 | "use strict" ; |
| 3 | const tiny = globalThis.tiny; |
| 4 | const stable = value => JSON.stringify(value, function(key,item) { return item && !Array.isArray(item) && typeof item === "object" ? Object.fromEntries(Object.keys(item).sort().map(name=>[name,item[name]])) : item; }); |
| 5 | class GrantRequest extends HTMLElement { |
| 6 | connectedCallback() { |
| 7 | if (!this.onClick) { |
| 8 | this.onClick = event => { |
| 9 | const button = event.target.closest("[data-grant-approve], [data-grant-deny]" ); |
| 10 | if (button && this.contains(button)) this.decide(button, button.hasAttribute("data-grant-approve" )); |
| 11 | }; |
| 12 | this.addEventListener("click" , this.onClick); |
| 13 | } |
| 14 | this.setBusy(false); |
| 15 | } |
| 16 | disconnectedCallback() { this.controller?.abort(); } |
| 17 | setBusy(busy) { |
| 18 | this.busy = busy; |
| 19 | this.querySelectorAll("button" ).forEach(button => { |
| 20 | button.disabled = busy || (button.hasAttribute("data-grant-approve" ) && this.hasAttribute("data-stale" )); |
| 21 | }); |
| 22 | } |
| 23 | report(text) { |
| 24 | let output = this.querySelector("output" ); |
| 25 | if (!output) { output = document.createElement("output" ); output.setAttribute("role" , "status" ); this.append(output); } |
| 26 | output.textContent = text; |
| 27 | } |
| 28 | async latest(id) { |
| 29 | const query = new URLSearchParams({method: "browseapproval" , params: JSON.stringify([{id}])}); |
| 30 | const response = await fetch(tiny.localPath("/webmcp/query" ) + "?" + query, {credentials: "same-origin" , cache: "no-store" , signal: this.controller.signal}); |
| 31 | const value = await response.json(); |
| 32 | if (!response.ok || value?.error) throw Error(value?.error?.message || value?.error || "Could not refresh this request." ); |
| 33 | return value.result || value; |
| 34 | } |
| 35 | async checkSigner(signer, operator) { |
| 36 | if (!signer?.getPublicKey || await signer.getPublicKey() !== operator || tiny.signer() !== signer) throw Error("Connect the requested operator's signer." ); |
| 37 | if (!this.isConnected || this.controller.signal.aborted) throw Error("This request is no longer open." ); |
| 38 | } |
| 39 | async decide(button, approve) { |
| 40 | if (this.busy) return; |
| 41 | this.setBusy(true); |
| 42 | this.controller = new AbortController(); |
| 43 | try { |
| 44 | const review = JSON.parse(this.getAttribute("data-review" ) || "{}" ); |
| 45 | const signer = tiny.signer(); |
| 46 | await this.checkSigner(signer, review.operator); |
| 47 | this.report("Checking current permissions…" ); |
| 48 | const {item} = await this.latest(review.request_id), fresh = item?.grant; |
| 49 | if (!item || item.id !== review.request_id || item.type !== "grant" || item.state !== "open" || item.asker !== review.agent || item.asked?.length !== 1 || item.asked[0] !== review.operator) throw Error("This request is no longer waiting for your decision." ); |
| 50 | if (approve && (!fresh || item.grant_error || fresh.request_id !== review.request_id || fresh.base !== review.base || fresh.agent !== review.agent || fresh.operator !== review.operator || stable(fresh.before) !== stable(review.before) || stable(fresh.after) !== stable(review.after))) throw Error("Permissions changed. Reload and review this request again." ); |
| 51 | await this.checkSigner(signer, review.operator); |
| 52 | const unsigned = approve ? fresh.unsigned : {kind: 7, created_at: Math.floor(Date.now()/1000), tags: [["e" , review.request_id, "" , review.agent], ["p" , review.agent], ["k" , "1111" ]], content: "-" }; |
| 53 | if (approve && (unsigned?.kind !== 30392 || unsigned.pubkey !== review.operator || !unsigned.tags.some(tag => tag[0] === "grant-request" && tag[1] === review.request_id) || !unsigned.tags.some(tag => tag[0] === "grant-base" && tag[1] === review.base))) throw Error("The grant review is incomplete." ); |
| 54 | this.report(approve ? "Signing grant…" : "Signing denial…" ); |
| 55 | await tiny.signing.publish({kind:unsigned.kind,created_at:unsigned.created_at,tags:unsigned.tags,content:unsigned.content}); |
| 56 | this.report(approve ? "Grant updated." : "Request denied." ); |
| 57 | if (this.isConnected) await tiny.navigate(location.href); |
| 58 | } catch (error) { |
| 59 | if (this.isConnected) { this.report(error.message || String(error)); this.setBusy(false); } |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | customElements.define("agent-grant-request" , GrantRequest); |
| 64 | })(); |