clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / tinyclient / grant-request.js
grant-request.js 4.2K
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})();