tinyrelayfile
tinyrelay/tinyclient/grant-request.js/

tinyclient/grant-request.js raw
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})();

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs