@spendctl/proxy 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +83 -0
- package/README.md +10 -2
- package/THIRD_PARTY_NOTICES +3771 -0
- package/dist/dashboard.d.ts +6 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +161 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +14 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +112 -0
- package/dist/node.js.map +1 -0
- package/dist/sidecar.d.ts +76 -0
- package/dist/sidecar.d.ts.map +1 -0
- package/dist/sidecar.js +708 -0
- package/dist/sidecar.js.map +1 -0
- package/dist/signers.d.ts +20 -0
- package/dist/signers.d.ts.map +1 -0
- package/dist/signers.js +58 -0
- package/dist/signers.js.map +1 -0
- package/dist/webhooks.d.ts +72 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +65 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +31 -3
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Approval, type Attempt } from "@spendctl/core";
|
|
2
|
+
/** Single-file local dashboard. The admin token is read from the URL fragment, never sent to the network except as a header. */
|
|
3
|
+
export declare function dashboardHtml(): string;
|
|
4
|
+
/** Review page behind an approval link. GET only displays; POST decides. */
|
|
5
|
+
export declare function reviewHtml(approval: Approval, attempt: Attempt | undefined, action: string, conflict: boolean): string;
|
|
6
|
+
//# sourceMappingURL=dashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAa,MAAM,gBAAgB,CAAC;AAuExE,gIAAgI;AAChI,wBAAgB,aAAa,IAAI,MAAM,CAkEtC;AASD,4EAA4E;AAC5E,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,GAChB,MAAM,CAsBR"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { formatUsd } from "@spendctl/core";
|
|
2
|
+
/**
|
|
3
|
+
* Single-file UI (no external assets, system fonts, inline SVG only) styled with the spendctl
|
|
4
|
+
* brand tokens from the 2026-10 design pass. Both themes derive from four colours; every neutral
|
|
5
|
+
* is a color-mix of fg over bg.
|
|
6
|
+
*/
|
|
7
|
+
const STYLE = `
|
|
8
|
+
:root{--bg:#fbfaf7;--fg:#17171a;--accent:#2f5bd3;--ok:#1f7a4d;--accent-fg:#fbfaf7;
|
|
9
|
+
--font-sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Hiragino Sans","Noto Sans JP","PingFang SC","Noto Sans SC",sans-serif;
|
|
10
|
+
--font-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--radius:6px;--focus-ring:0 0 0 2px var(--bg),0 0 0 4px var(--accent);color-scheme:light}
|
|
11
|
+
@media (prefers-color-scheme:dark){:root{--bg:#111216;--fg:#ecebe6;--accent:#7d9dff;--ok:#57c993;--accent-fg:#111216;color-scheme:dark}}
|
|
12
|
+
:root{--muted:color-mix(in srgb,var(--fg) 64%,var(--bg));--border:color-mix(in srgb,var(--fg) 13%,var(--bg));--border-strong:color-mix(in srgb,var(--fg) 28%,var(--bg));
|
|
13
|
+
--surface:color-mix(in srgb,var(--fg) 3%,var(--bg));--surface-2:color-mix(in srgb,var(--fg) 6%,var(--bg))}
|
|
14
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
15
|
+
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font-sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
16
|
+
h1,h2,h3,p{margin:0}a{color:var(--accent)}code,.mono{font-family:var(--font-mono);font-size:12.5px}
|
|
17
|
+
:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:var(--radius)}
|
|
18
|
+
.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
19
|
+
.top{display:flex;align-items:center;gap:16px;padding:12px max(16px,env(safe-area-inset-right,0px)) 12px max(16px,env(safe-area-inset-left,0px));border-bottom:1px solid var(--border)}
|
|
20
|
+
.top .mark{width:22px;height:22px}.top h1{font-size:15px;font-weight:600}.top .where{color:var(--muted);font-size:13px}
|
|
21
|
+
.status{margin-left:auto;display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:12px;color:var(--muted)}
|
|
22
|
+
.status i{width:8px;height:8px;border-radius:50%;background:var(--ok)}.status.off i{background:var(--muted)}.status.dry i{background:var(--accent)}
|
|
23
|
+
main{max-width:1120px;margin:0 auto;padding:24px max(16px,env(safe-area-inset-right,0px)) 64px max(16px,env(safe-area-inset-left,0px));display:grid;gap:32px}
|
|
24
|
+
section{display:grid;gap:12px}
|
|
25
|
+
.sec-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}.sec-head h2{font-size:16px;font-weight:600}
|
|
26
|
+
.sec-head .count{font-family:var(--font-mono);font-size:12px;color:var(--muted)}.sec-head .right{margin-left:auto;font-size:13px}
|
|
27
|
+
.budgets{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
|
|
28
|
+
.budget{border:1px solid var(--border);border-radius:10px;background:var(--surface);padding:16px;display:grid;gap:12px;min-width:0}
|
|
29
|
+
.budget .who{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}.budget .who b{font-weight:600}
|
|
30
|
+
.budget .who .rules{font-family:var(--font-mono);font-size:12px;color:var(--muted)}
|
|
31
|
+
.meter{display:grid;gap:5px}.meter .row{display:flex;justify-content:space-between;gap:12px;font-size:13px}
|
|
32
|
+
.meter .row span:last-child{font-family:var(--font-mono);font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
|
33
|
+
.bar{height:6px;border-radius:3px;background:var(--surface-2);overflow:hidden}.bar i{display:block;height:100%;background:var(--accent);border-radius:3px}.bar.full i{background:var(--fg)}
|
|
34
|
+
.tbl{width:100%;border-collapse:collapse;font-size:13px}
|
|
35
|
+
.tbl th,.tbl td{text-align:left;padding:10px 12px;border-top:1px solid var(--border);vertical-align:middle;white-space:nowrap}
|
|
36
|
+
.tbl th{font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);font-weight:500;border-top:0;padding-top:0}
|
|
37
|
+
.tbl td.num{font-family:var(--font-mono);font-variant-numeric:tabular-nums;text-align:right}.tbl th.num{text-align:right}
|
|
38
|
+
.tbl td.why{white-space:normal;min-width:220px;color:var(--muted)}
|
|
39
|
+
.tbl .hash{font-family:var(--font-mono);font-size:12px;color:var(--muted);white-space:normal;min-width:200px;max-width:360px}
|
|
40
|
+
.tblwrap{border:1px solid var(--border);border-radius:10px;background:var(--surface);padding:12px 4px 4px;overflow-x:auto}
|
|
41
|
+
.pill{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:12px}
|
|
42
|
+
.pill::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--muted)}
|
|
43
|
+
.pill.ok{color:var(--ok)}.pill.ok::before{background:var(--ok)}.pill.held{color:var(--accent)}.pill.held::before{background:var(--accent)}
|
|
44
|
+
.pill.unknown::before{background:transparent;border:1.5px solid var(--muted)}
|
|
45
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:6px 12px;border-radius:6px;border:1px solid transparent;font:inherit;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}
|
|
46
|
+
.btn-primary{background:var(--accent);color:var(--accent-fg);border-color:var(--accent)}.btn-primary:hover{background:color-mix(in srgb,var(--accent) 88%,var(--fg))}
|
|
47
|
+
.btn-secondary{background:transparent;color:var(--fg);border-color:var(--border-strong)}.btn-secondary:hover{background:var(--surface-2);border-color:var(--fg)}
|
|
48
|
+
.btn-link{background:none;border:0;padding:0;min-height:0;color:var(--accent);font-size:13px;font-weight:400;text-decoration:underline;cursor:pointer}
|
|
49
|
+
.actions{display:flex;gap:8px}.expires{font-family:var(--font-mono);font-size:12px;color:var(--muted)}
|
|
50
|
+
.empty{border:1px dashed var(--border-strong);border-radius:10px;padding:28px 20px;display:grid;gap:8px;justify-items:start;color:var(--muted)}
|
|
51
|
+
.empty b{color:var(--fg);font-weight:600}.empty code{background:var(--surface-2);border:1px solid var(--border);padding:1px 6px;border-radius:4px;color:var(--fg)}
|
|
52
|
+
.notice{color:var(--muted);font-size:13px}
|
|
53
|
+
@media(max-width:640px){.top{flex-wrap:wrap}.status{margin-left:0;width:100%}main{padding-top:16px;gap:24px}
|
|
54
|
+
.tblwrap{padding:4px 12px}
|
|
55
|
+
.tbl.stack,.tbl.stack tbody,.tbl.stack tr,.tbl.stack td{display:block}.tbl.stack thead{display:none}
|
|
56
|
+
.tbl.stack tr{padding:12px 0;border-top:1px solid var(--border)}.tbl.stack tr:first-child{border-top:0}
|
|
57
|
+
.tbl.stack td{border:0;padding:3px 0;white-space:normal;text-align:left;display:flex;gap:12px;justify-content:space-between;align-items:baseline}
|
|
58
|
+
.tbl.stack td::before{content:attr(data-l);font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);flex:none;min-width:64px}
|
|
59
|
+
.tbl.stack td.why{display:block;color:var(--muted)}.tbl.stack td.why::before{display:block;margin-bottom:2px}
|
|
60
|
+
.tbl.stack td.act{display:block;padding-top:10px}.tbl.stack td.act::before{display:none}.tbl.stack .actions .btn{flex:1}
|
|
61
|
+
.tbl.stack td.hash{max-width:none;min-width:0}}
|
|
62
|
+
`;
|
|
63
|
+
const MARK = `<svg class="mark" viewBox="0 0 24 24" role="img" aria-label="spendctl"><path d="M6 6H18M6 12H12M6 18H9" fill="none" stroke="var(--accent)" stroke-width="3" stroke-linecap="round"/><path d="M15 18a2.25 2.25 0 1 0 4.5 0a2.25 2.25 0 1 0 -4.5 0Z" fill="var(--ok)"/></svg>`;
|
|
64
|
+
/** Favicon as a data URI so the browser does not send /favicon.ico through the gateway. */
|
|
65
|
+
const FAVICON = `<link rel="icon" href="data:image/svg+xml;utf8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="5.25" fill="#111216"/><path d="M6 6H18M6 12H12M6 18H9" fill="none" stroke="#7d9dff" stroke-width="3" stroke-linecap="round"/><path d="M15 18a2.25 2.25 0 1 0 4.5 0a2.25 2.25 0 1 0 -4.5 0Z" fill="#57c993"/></svg>')}">`;
|
|
66
|
+
/** Single-file local dashboard. The admin token is read from the URL fragment, never sent to the network except as a header. */
|
|
67
|
+
export function dashboardHtml() {
|
|
68
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
69
|
+
<meta name="robots" content="noindex"><title>spendctl local dashboard</title>${FAVICON}<style>${STYLE}</style></head><body>
|
|
70
|
+
<header class="top">${MARK}<h1>spendctl</h1><span class="where" id="where">local dashboard</span><span class="status off" id="status"><i aria-hidden="true"></i><span id="status-text">connecting…</span></span></header>
|
|
71
|
+
<main>
|
|
72
|
+
<section aria-labelledby="h-budgets"><div class="sec-head"><h2 id="h-budgets">Budgets</h2><span class="count" id="agents-count"></span></div><div class="budgets" id="agents"></div></section>
|
|
73
|
+
<section aria-labelledby="h-pending"><div class="sec-head"><h2 id="h-pending">Waiting for approval</h2><span class="count" id="pending-count"></span></div><div id="pending"></div></section>
|
|
74
|
+
<section aria-labelledby="h-recent"><div class="sec-head"><h2 id="h-recent">Recent payments</h2><span class="count" id="recent-count"></span><button class="btn-link right" type="button" id="export">Export CSV</button></div><div id="recent"></div></section>
|
|
75
|
+
</main><script>
|
|
76
|
+
const token = new URLSearchParams(location.hash.slice(1)).get("token") || sessionStorage.getItem("spendctl-token") || "";
|
|
77
|
+
if (token) { try { sessionStorage.setItem("spendctl-token", token); } catch {} history.replaceState(null, "", location.pathname); }
|
|
78
|
+
const h = (tag, attrs = {}, ...kids) => { const el = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) k === "class" ? el.className = v : el.setAttribute(k, v); for (const kid of kids) el.append(kid); return el; };
|
|
79
|
+
const api = (path, init = {}) => fetch("/_spendctl/admin/" + path, { ...init, headers: { authorization: "Bearer " + token, "content-type": "application/json" } }).then(r => r.ok ? r.json() : Promise.reject(new Error(r.status + "")));
|
|
80
|
+
const money = s => Number(String(s).replace("$", ""));
|
|
81
|
+
const usd = micros => micros === undefined || micros === null ? "—" : "$" + (Number(micros) / 1e6).toFixed(2);
|
|
82
|
+
const cap = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
83
|
+
const pillClass = status => status === "settled" ? "pill ok" : status === "pending_approval" || status === "reserved" ? "pill held" : status === "unknown" ? "pill unknown" : "pill";
|
|
84
|
+
const td = (label, cls, ...kids) => h("td", { class: cls, "data-l": label }, ...kids);
|
|
85
|
+
document.getElementById("where").textContent = "local dashboard · " + location.host;
|
|
86
|
+
function setStatus(text, cls) { document.getElementById("status").className = "status " + cls; document.getElementById("status-text").textContent = text; }
|
|
87
|
+
async function decide(id, action) { await api("approvals/" + id + "/" + action, { method: "POST", body: JSON.stringify({ by: "dashboard" }) }); refresh(); }
|
|
88
|
+
document.getElementById("export").onclick = async () => {
|
|
89
|
+
const r = await fetch("/_spendctl/admin/export?format=csv", { headers: { authorization: "Bearer " + token } });
|
|
90
|
+
if (!r.ok) return;
|
|
91
|
+
const a = document.createElement("a"); a.href = URL.createObjectURL(await r.blob()); a.download = "receipts.csv"; a.click(); URL.revokeObjectURL(a.href);
|
|
92
|
+
};
|
|
93
|
+
async function refresh() {
|
|
94
|
+
try {
|
|
95
|
+
const s = await api("summary");
|
|
96
|
+
const mode = s.dryRun ? "dry-run" : (s.agents[0]?.mode ?? "enforce");
|
|
97
|
+
setStatus("sidecar running · policy " + mode + " · " + s.agents.length + " agent" + (s.agents.length === 1 ? "" : "s"), s.dryRun ? "dry" : "");
|
|
98
|
+
document.getElementById("agents-count").textContent = s.agents.length + " agent" + (s.agents.length === 1 ? "" : "s");
|
|
99
|
+
const agents = document.getElementById("agents"); agents.replaceChildren();
|
|
100
|
+
for (const a of s.agents) {
|
|
101
|
+
const rules = [a.perRequest ? "max " + a.perRequest + "/request" : "", a.requireApprovalAbove ? "approval ≥ " + a.requireApprovalAbove : "", a.allow && !(a.allow.length === 1 && a.allow[0] === "*") ? "allowlist " + a.allow.length + " host" + (a.allow.length === 1 ? "" : "s") : ""].filter(Boolean).join(" · ");
|
|
102
|
+
const card = h("div", { class: "budget" }, h("div", { class: "who" }, h("b", {}, a.agent), h("span", { class: "rules" }, rules || a.mode)));
|
|
103
|
+
for (const w of a.windows) {
|
|
104
|
+
const limit = money(w.limit), spent = money(w.spent); const pct = Math.min(100, spent / Math.max(limit, 1e-9) * 100);
|
|
105
|
+
card.append(h("div", { class: "meter" }, h("div", { class: "row" }, h("span", {}, cap(w.window)), h("span", {}, w.spent + " of " + w.limit + " · " + w.remaining + " left")), h("div", { class: "bar" + (spent >= limit ? " full" : "") }, h("i", { style: "width:" + pct + "%" }))));
|
|
106
|
+
}
|
|
107
|
+
if (!a.windows.length) card.append(h("div", { class: "notice" }, "No time-window budget — only per-request and host rules apply."));
|
|
108
|
+
agents.append(card);
|
|
109
|
+
}
|
|
110
|
+
const p = await api("pending"); const pending = document.getElementById("pending");
|
|
111
|
+
document.getElementById("pending-count").textContent = p.pending.length ? p.pending.length + " held" : "none";
|
|
112
|
+
if (!p.pending.length) pending.replaceChildren(h("p", { class: "notice" }, "Nothing is waiting. Requests above a threshold or over budget with onBudgetExceeded: approve show up here."));
|
|
113
|
+
else {
|
|
114
|
+
const rows = p.pending.map(x => {
|
|
115
|
+
const approve = h("button", { class: "btn btn-primary", type: "button" }, "Approve"); approve.onclick = () => decide(x.id, "approve");
|
|
116
|
+
const deny = h("button", { class: "btn btn-secondary", type: "button" }, "Deny"); deny.onclick = () => decide(x.id, "deny");
|
|
117
|
+
const left = Math.max(0, Math.round((x.expiresAt - Date.now()) / 1000));
|
|
118
|
+
return h("tr", {}, td("ID", "mono", x.id), td("Agent", "", x.agent), td("Endpoint", "mono", x.host + x.path), td("Amount", "num", usd(x.usdMicros)), td("Expires", "expires", left + " s"), td("Why", "why", x.reasons.map(r => r.message).join(" ")), h("td", { class: "act" }, h("div", { class: "actions" }, approve, deny)));
|
|
119
|
+
});
|
|
120
|
+
pending.replaceChildren(h("div", { class: "tblwrap" }, h("table", { class: "tbl stack" }, h("thead", {}, h("tr", {}, h("th", {}, "ID"), h("th", {}, "Agent"), h("th", {}, "Endpoint"), h("th", { class: "num" }, "Amount"), h("th", {}, "Expires"), h("th", {}, "Why"), h("th", {}, h("span", { class: "visually-hidden" }, "Actions")))), h("tbody", {}, ...rows))));
|
|
121
|
+
}
|
|
122
|
+
const recent = document.getElementById("recent");
|
|
123
|
+
document.getElementById("recent-count").textContent = s.recent.length ? "latest " + s.recent.length + " · UTC" : "0";
|
|
124
|
+
if (!s.recent.length) recent.replaceChildren(h("div", { class: "empty" }, h("b", {}, "No payments yet."), h("span", {}, "Point an agent at the sidecar and the first governed call will show up here, with its receipt and tx hash."), h("code", {}, "SPENDCTL_URL=" + location.origin + " node agent.mjs")));
|
|
125
|
+
else {
|
|
126
|
+
const rows = s.recent.map(r => h("tr", {}, td("Time", "mono", r.created_at_utc.replace("T", " ").slice(11, 19)), td("ID", "mono", r.id), td("Agent", "", r.agent), td("Endpoint", "mono", r.method + " " + r.host + r.path), td("USD", "num", r.usd_value ? "$" + r.usd_value : "—"), td("Status", "", h("span", { class: pillClass(r.status) }, r.status)), td(r.tx_hash ? "Tx" : "Reason", "hash", r.tx_hash ? r.tx_hash.slice(0, 14) + "…" : r.reasons)));
|
|
127
|
+
recent.replaceChildren(h("div", { class: "tblwrap" }, h("table", { class: "tbl stack" }, h("thead", {}, h("tr", {}, h("th", {}, "Time"), h("th", {}, "ID"), h("th", {}, "Agent"), h("th", {}, "Endpoint"), h("th", { class: "num" }, "USD"), h("th", {}, "Status"), h("th", {}, "Tx / reason"))), h("tbody", {}, ...rows))));
|
|
128
|
+
}
|
|
129
|
+
} catch (e) { setStatus(token ? "cannot reach the admin API (" + e.message + ")" : "open this page with: spendctl dashboard", "off"); }
|
|
130
|
+
}
|
|
131
|
+
refresh(); setInterval(refresh, 2000);
|
|
132
|
+
</script></body></html>`;
|
|
133
|
+
}
|
|
134
|
+
function escapeHtml(text) {
|
|
135
|
+
return text.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
136
|
+
}
|
|
137
|
+
/** Review page behind an approval link. GET only displays; POST decides. */
|
|
138
|
+
export function reviewHtml(approval, attempt, action, conflict) {
|
|
139
|
+
const amount = approval.usdMicros !== undefined ? formatUsd(approval.usdMicros) : "unpriced";
|
|
140
|
+
const pending = approval.status === "pending" && approval.expiresAt > Date.now();
|
|
141
|
+
const details = [
|
|
142
|
+
["Agent", approval.agent],
|
|
143
|
+
["Endpoint", `${attempt?.method ?? ""} ${approval.host}${approval.path}`],
|
|
144
|
+
["Amount", amount],
|
|
145
|
+
["Network / asset", `${attempt?.network ?? "-"} / ${attempt?.assetSymbol ?? attempt?.asset ?? "-"}`],
|
|
146
|
+
["Recipient", attempt?.payTo ?? "-"],
|
|
147
|
+
["Why approval is needed", approval.reasons.map((reason) => reason.message).join(" ")],
|
|
148
|
+
["Status", approval.status],
|
|
149
|
+
["Expires", new Date(approval.expiresAt).toISOString()],
|
|
150
|
+
];
|
|
151
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
152
|
+
<meta name="robots" content="noindex"><title>Approve payment · spendctl</title>${FAVICON}<style>${STYLE}</style></head><body>
|
|
153
|
+
<header class="top">${MARK}<h1>spendctl</h1><span class="where">payment approval</span></header>
|
|
154
|
+
<main style="max-width:640px">
|
|
155
|
+
<section><div class="sec-head"><h2>Payment approval</h2><span class="count">${escapeHtml(approval.id)}</span></div>
|
|
156
|
+
${conflict ? `<p class="notice">This approval was already decided or has expired.</p>` : ""}
|
|
157
|
+
<div class="tblwrap"><table class="tbl">${details.map(([k, v]) => `<tr><th scope="row">${escapeHtml(k ?? "")}</th><td style="white-space:normal">${escapeHtml(v ?? "")}</td></tr>`).join("")}</table></div>
|
|
158
|
+
${pending ? `<form method="post" action="${escapeHtml(action)}" class="actions"><button class="btn btn-primary" name="action" value="approve">Approve ${escapeHtml(amount)}</button><button class="btn btn-secondary" name="action" value="deny">Deny</button></form>` : ""}
|
|
159
|
+
</section></main></body></html>`;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { dashboardHtml } from "./dashboard.js";
|
|
2
|
+
export { type ListeningSidecar, listen } from "./node.js";
|
|
3
|
+
export { Sidecar, type SidecarEvent, type SidecarOptions } from "./sidecar.js";
|
|
4
|
+
export { createSigner, requestTestnetUsdc, type SignerInfo } from "./signers.js";
|
|
5
|
+
export { sendWebhooks, webhookBody } from "./webhooks.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { dashboardHtml } from "./dashboard.js";
|
|
2
|
+
export { listen } from "./node.js";
|
|
3
|
+
export { Sidecar } from "./sidecar.js";
|
|
4
|
+
export { createSigner, requestTestnetUsdc } from "./signers.js";
|
|
5
|
+
export { sendWebhooks, webhookBody } from "./webhooks.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAyB,MAAM,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,OAAO,EAA0C,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAmB,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { Sidecar } from "./sidecar.js";
|
|
3
|
+
export interface ListeningSidecar {
|
|
4
|
+
url: string;
|
|
5
|
+
port: number;
|
|
6
|
+
server: Server;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** Serves the sidecar on node:http: gateway paths, absolute-URI forward proxying and CONNECT tunnels. */
|
|
10
|
+
export declare function listen(sidecar: Sidecar, options?: {
|
|
11
|
+
port?: number;
|
|
12
|
+
host?: string;
|
|
13
|
+
}): Promise<ListeningSidecar>;
|
|
14
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAEjG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiED,yGAAyG;AACzG,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqClH"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { connect } from "node:net";
|
|
3
|
+
async function readBody(req) {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for await (const chunk of req)
|
|
6
|
+
chunks.push(chunk);
|
|
7
|
+
return Buffer.concat(chunks);
|
|
8
|
+
}
|
|
9
|
+
function toRequest(req, body, local, signal) {
|
|
10
|
+
const headers = new Headers();
|
|
11
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
for (const v of value)
|
|
14
|
+
headers.append(key, v);
|
|
15
|
+
else if (value !== undefined)
|
|
16
|
+
headers.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
const raw = req.url ?? "/";
|
|
19
|
+
// Forward-proxy requests (HTTP_PROXY) carry an absolute URI in the request line.
|
|
20
|
+
if (/^https?:\/\//i.test(raw))
|
|
21
|
+
headers.set("x-spendctl-target", raw);
|
|
22
|
+
const method = req.method ?? "GET";
|
|
23
|
+
return new Request(new URL(/^https?:\/\//i.test(raw) ? "/" : raw, local), {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: method === "GET" || method === "HEAD" || body.length === 0 ? undefined : new Uint8Array(body),
|
|
27
|
+
signal,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function writeResponse(res, response) {
|
|
31
|
+
const headers = {};
|
|
32
|
+
response.headers.forEach((value, key) => {
|
|
33
|
+
if (key !== "set-cookie")
|
|
34
|
+
headers[key] = value;
|
|
35
|
+
});
|
|
36
|
+
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
37
|
+
if (cookies.length)
|
|
38
|
+
headers["set-cookie"] = cookies;
|
|
39
|
+
res.writeHead(response.status, response.statusText || undefined, headers);
|
|
40
|
+
if (response.body) {
|
|
41
|
+
const reader = response.body.getReader();
|
|
42
|
+
for (;;) {
|
|
43
|
+
const { done, value } = await reader.read();
|
|
44
|
+
if (done)
|
|
45
|
+
break;
|
|
46
|
+
res.write(value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
res.end();
|
|
50
|
+
}
|
|
51
|
+
function handleConnect(sidecar, req, client, head) {
|
|
52
|
+
if (sidecar.config.server.connect === "deny") {
|
|
53
|
+
client.end("HTTP/1.1 403 Forbidden\r\ncontent-type: text/plain\r\n\r\nspendctl: HTTPS tunnelling is disabled. Use the gateway form http://<sidecar>/https://... so payments can be governed.\n");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Blind tunnel: spendctl cannot see inside TLS, so a 402 here reaches the agent
|
|
57
|
+
// unpaid (the agent holds no keys). Use the gateway form to have payments handled.
|
|
58
|
+
const [host, port] = (req.url ?? "").split(":");
|
|
59
|
+
const upstream = connect(Number(port) || 443, host ?? "", () => {
|
|
60
|
+
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
61
|
+
if (head.length)
|
|
62
|
+
upstream.write(head);
|
|
63
|
+
upstream.pipe(client);
|
|
64
|
+
client.pipe(upstream);
|
|
65
|
+
});
|
|
66
|
+
upstream.on("error", () => client.destroy());
|
|
67
|
+
client.on("error", () => upstream.destroy());
|
|
68
|
+
}
|
|
69
|
+
/** Serves the sidecar on node:http: gateway paths, absolute-URI forward proxying and CONNECT tunnels. */
|
|
70
|
+
export function listen(sidecar, options = {}) {
|
|
71
|
+
const host = options.host ?? sidecar.config.server.host;
|
|
72
|
+
const server = createServer(async (req, res) => {
|
|
73
|
+
// Aborts when the client goes away before the response is complete, so a held
|
|
74
|
+
// payment is never made for a requester that is no longer listening.
|
|
75
|
+
const gone = new AbortController();
|
|
76
|
+
res.on("close", () => {
|
|
77
|
+
if (!res.writableFinished)
|
|
78
|
+
gone.abort();
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const body = await readBody(req);
|
|
82
|
+
const request = toRequest(req, body, `http://${req.headers.host ?? host}`, gone.signal);
|
|
83
|
+
const response = await sidecar.handle(request);
|
|
84
|
+
if (!res.destroyed)
|
|
85
|
+
await writeResponse(res, response);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (res.destroyed)
|
|
89
|
+
return;
|
|
90
|
+
if (!res.headersSent)
|
|
91
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
92
|
+
res.end(JSON.stringify({ error: "spendctl_internal_error", message: error.message }));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
server.on("connect", (req, socket, head) => handleConnect(sidecar, req, socket, head));
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
server.once("error", reject);
|
|
98
|
+
server.listen(options.port ?? sidecar.config.server.port, host, () => {
|
|
99
|
+
const { port } = server.address();
|
|
100
|
+
resolve({
|
|
101
|
+
url: `http://${host}:${port}`,
|
|
102
|
+
port,
|
|
103
|
+
server,
|
|
104
|
+
close: () => new Promise((done) => {
|
|
105
|
+
server.closeAllConnections?.();
|
|
106
|
+
server.close(() => done());
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type MppCredentialProvider } from "@spendctl/adapter-mpp";
|
|
2
|
+
import { type ClientEvmSigner } from "@spendctl/adapter-x402";
|
|
3
|
+
import { type Approval, ApprovalBroker, type Attempt, type Config, type Ledger } from "@spendctl/core";
|
|
4
|
+
export interface SidecarEvent {
|
|
5
|
+
type: "decision" | "settled" | "failed" | "approval" | "error";
|
|
6
|
+
attempt?: Attempt;
|
|
7
|
+
approval?: Approval;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SidecarOptions {
|
|
11
|
+
config: Config;
|
|
12
|
+
ledger: Ledger;
|
|
13
|
+
/** Signs x402 EVM payments. Without it, x402 options are unsignable. */
|
|
14
|
+
evmSigner?: ClientEvmSigner;
|
|
15
|
+
mppProviders?: MppCredentialProvider[];
|
|
16
|
+
/** Token required for the admin / approval API. Agents must never see it. */
|
|
17
|
+
adminToken: string;
|
|
18
|
+
/** Per-agent tokens (agent name → token), required when `agentAuth: token`. */
|
|
19
|
+
agentTokens?: Record<string, string>;
|
|
20
|
+
/** Global dry-run: evaluate and record, never sign. */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
/** Upstream fetch (tests inject one). */
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
/** Base URL of this sidecar, used in approval links. */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
onEvent?: (event: SidecarEvent) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The spendctl sidecar. `handle()` is a Fetch-API handler, so the same logic runs
|
|
30
|
+
* behind node:http (see `listen()`), Bun.serve, or a Workers-style runtime.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Sidecar {
|
|
33
|
+
readonly options: SidecarOptions;
|
|
34
|
+
readonly broker: ApprovalBroker;
|
|
35
|
+
private readonly circuits;
|
|
36
|
+
private readonly upstreamFetch;
|
|
37
|
+
constructor(options: SidecarOptions);
|
|
38
|
+
get config(): Config;
|
|
39
|
+
get ledger(): Ledger;
|
|
40
|
+
private emit;
|
|
41
|
+
private baseUrl;
|
|
42
|
+
/** One-time-ish review token: HMAC of the approval id under the admin token. */
|
|
43
|
+
approvalToken(id: string): string;
|
|
44
|
+
reviewLink(id: string): string;
|
|
45
|
+
handle(request: Request): Promise<Response>;
|
|
46
|
+
private identifyAgent;
|
|
47
|
+
private isAdmin;
|
|
48
|
+
private gateway;
|
|
49
|
+
private pay;
|
|
50
|
+
private signAndSend;
|
|
51
|
+
private finish;
|
|
52
|
+
private circuit;
|
|
53
|
+
private newAttempt;
|
|
54
|
+
private recordOnly;
|
|
55
|
+
private refusal;
|
|
56
|
+
private forward;
|
|
57
|
+
private passthrough;
|
|
58
|
+
budget(agent: string): {
|
|
59
|
+
agent: string;
|
|
60
|
+
mode: "dry-run" | "enforce";
|
|
61
|
+
perRequest: string | null;
|
|
62
|
+
requireApprovalAbove: string | null;
|
|
63
|
+
onBudgetExceeded: "approve" | "deny";
|
|
64
|
+
allow: string[];
|
|
65
|
+
windows: {
|
|
66
|
+
window: "daily" | "hourly" | "monthly" | "weekly";
|
|
67
|
+
limit: string;
|
|
68
|
+
spent: string;
|
|
69
|
+
remaining: string;
|
|
70
|
+
}[];
|
|
71
|
+
} | undefined;
|
|
72
|
+
private api;
|
|
73
|
+
private decideApproval;
|
|
74
|
+
private review;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=sidecar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,qBAAqB,EAI3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,eAAe,EAMrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,QAAQ,EACb,cAAc,EACd,KAAK,OAAO,EAGZ,KAAK,MAAM,EAMX,KAAK,MAAM,EAeZ,MAAM,gBAAgB,CAAC;AAIxB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,uDAAuD;IACvD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yCAAyC;IACzC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CACzC;AA8CD;;;GAGG;AACH,qBAAa,OAAO;IAKN,QAAQ,CAAC,OAAO,EAAE,cAAc;IAJ5C,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAE7C,YAAqB,OAAO,EAAE,cAAc,EAY3C;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,OAAO;IAQf,gFAAgF;IAChF,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhC;IAED,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7B;IAIK,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmBhD;IAED,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,OAAO;YAQD,OAAO;YAgEP,GAAG;YA8KH,WAAW;IAqFzB,OAAO,CAAC,MAAM;IAiBd,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,UAAU;IAwClB,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,OAAO;YA4BD,OAAO;IAsBrB,OAAO,CAAC,WAAW;IAmBnB,MAAM,CAAC,KAAK,EAAE,MAAM;;;;;;;;;;;;;kBAyBnB;YAEa,GAAG;IA0GjB,OAAO,CAAC,cAAc;YAYR,MAAM;CAuBrB"}
|