@agent-custody/receipts 0.6.8 → 0.6.10
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/dist/log-admin.js +15 -5
- package/dist/portal.js +20 -6
- package/package.json +1 -1
package/dist/log-admin.js
CHANGED
|
@@ -104,7 +104,7 @@ export function adminRoutes(opts) {
|
|
|
104
104
|
return json(401, { error: "admin token required" }, { "www-authenticate": 'Basic realm="agent-custody log admin", charset="UTF-8"' }), true;
|
|
105
105
|
}
|
|
106
106
|
if (req.method === "GET" && url.pathname === "/admin") {
|
|
107
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-frame-options": "DENY", "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'" });
|
|
107
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-frame-options": "DENY", "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self'; connect-src 'self'" });
|
|
108
108
|
res.end(ADMIN_PAGE);
|
|
109
109
|
return true;
|
|
110
110
|
}
|
|
@@ -204,9 +204,12 @@ export function adminRoutes(opts) {
|
|
|
204
204
|
const ADMIN_PAGE = `<!doctype html>
|
|
205
205
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
206
206
|
<title>agent-custody log admin</title>
|
|
207
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
208
|
+
<script>(() => { try { const t = localStorage.getItem("agent-custody-theme"); if (t === "dark" || t === "light") document.documentElement.dataset.theme = t; } catch {} })();</script>
|
|
207
209
|
<style>
|
|
208
210
|
:root { color-scheme: light dark; --ink: #1b2430; --ink2: #5b6b7a; --line: #d7dfe5; --bg: #fafbfc; --panel: #ffffff; --accent: #0f6e63; --warn: #8a5a00; --warnbg: #fbf1dc; --mono: ui-monospace, Menlo, monospace; }
|
|
209
|
-
@media (prefers-color-scheme: dark) { :root { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #4fc3b0; --warn: #e2b862; --warnbg: #2d2412; } }
|
|
211
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #4fc3b0; --warn: #e2b862; --warnbg: #2d2412; } }
|
|
212
|
+
:root[data-theme="dark"] { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #4fc3b0; --warn: #e2b862; --warnbg: #2d2412; }
|
|
210
213
|
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
|
|
211
214
|
main { max-width: 72rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
212
215
|
h1 { font-size: 1.4rem; margin: 0 0 .25rem; } h2 { font-size: 1.05rem; margin: 2rem 0 .75rem; }
|
|
@@ -225,14 +228,16 @@ const ADMIN_PAGE = `<!doctype html>
|
|
|
225
228
|
.once { border-left: 3px solid var(--warn); background: var(--warnbg); padding: .8rem 1rem; border-radius: 0 4px 4px 0; margin: 1rem 0; }
|
|
226
229
|
.muted { color: var(--ink2); } .err { color: #b3261e; } .ok { color: var(--accent); }
|
|
227
230
|
.tok { font-family: var(--mono); font-size: 1.05rem; word-break: break-all; user-select: all; }
|
|
231
|
+
.head { display: flex; justify-content: space-between; align-items: start; gap: 1rem; }
|
|
232
|
+
button.theme { background: transparent; color: var(--ink2); border: 1px solid var(--line); padding: .25rem .6rem; font-size: .8rem; border-radius: 4px; } button.theme:hover { color: var(--ink); }
|
|
228
233
|
[hidden] { display: none !important; }
|
|
229
234
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .6rem; margin: 0 0 1rem; }
|
|
230
235
|
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; padding: .6rem .8rem; }
|
|
231
236
|
.stat b { display: block; font-size: 1.4rem; font-variant-numeric: tabular-nums; } .stat span { color: var(--ink2); font-size: .8rem; }
|
|
232
237
|
</style>
|
|
233
238
|
<main>
|
|
234
|
-
<h1>Log admin</h1>
|
|
235
|
-
<p class="sub" id="where">Tenants and tokens on this log.</p>
|
|
239
|
+
<div class="head"><div><h1>Log admin</h1>
|
|
240
|
+
<p class="sub" id="where">Tenants and tokens on this log.</p></div><button class="theme" id="themeToggle" type="button">Dark mode</button></div>
|
|
236
241
|
<section id="app">
|
|
237
242
|
<h2>Registrations</h2>
|
|
238
243
|
<div class="row"><label>month<input id="rmonth" type="month"></label><button class="quiet" id="loadRegs">Show</button><a id="rcsv" class="quiet" href="#" style="align-self:center">Download CSV</a></div>
|
|
@@ -283,6 +288,11 @@ const ADMIN_PAGE = `<!doctype html>
|
|
|
283
288
|
};
|
|
284
289
|
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
285
290
|
const say = (t, cls) => { $("msg").textContent = t; $("msg").className = cls || "muted"; };
|
|
291
|
+
// Light or dark by choice, kept in this browser; unset, the page follows the system. The button says where it is going.
|
|
292
|
+
const themeNow = () => document.documentElement.dataset.theme || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
293
|
+
const themeLabel = () => { $("themeToggle").textContent = themeNow() === "dark" ? "Light mode" : "Dark mode"; };
|
|
294
|
+
$("themeToggle").onclick = () => { const next = themeNow() === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = next; try { localStorage.setItem("agent-custody-theme", next); } catch {} themeLabel(); };
|
|
295
|
+
themeLabel();
|
|
286
296
|
const loadTenants = async () => {
|
|
287
297
|
const list = await api("GET", "/admin/tenants");
|
|
288
298
|
const planPick = (t) => "<select data-plan=\\"" + esc(t.id) + "\\">" + ["free", "team", "enterprise"].map((p) => "<option" + (p === t.plan ? " selected" : "") + ">" + p + "</option>").join("") + "</select>";
|
|
@@ -327,7 +337,7 @@ const ADMIN_PAGE = `<!doctype html>
|
|
|
327
337
|
$("rcsv").href = "/admin/registrations.csv?month=" + encodeURIComponent(month);
|
|
328
338
|
$("sTenants").textContent = r.totals.tenants; $("sRegistered").textContent = r.totals.registered; $("sAppends").textContent = r.totals.appends; $("sLeaves").textContent = r.totals.totalLeaves;
|
|
329
339
|
const who = (x) => x.email ? esc(x.name || "") + (x.company ? " <span class=muted>· " + esc(x.company) + (x.role ? ", " + esc(x.role) : "") + "</span>" : "") : "<span class=muted>onboarded by script</span>";
|
|
330
|
-
const reach = (x) => [x.email ? "<a href
|
|
340
|
+
const reach = (x) => [x.email ? "<a href=\\"mailto:" + esc(x.email) + "\\">" + esc(x.email) + "</a>" : "", x.phone ? esc(x.phone) : "", x.telegram ? "telegram @" + esc(x.telegram) : ""].filter(Boolean).join("<br>");
|
|
331
341
|
$("regs").innerHTML = r.rows.map((x) => "<tr><td>" + who(x) + "</td><td>" + reach(x) + "</td><td><code>" + esc(x.tenant) + "</code>" + (x.disabled ? " <span class=muted>disabled</span>" : "") + "</td><td>" + esc(x.plan) + "</td><td>" + (x.registeredAt ? esc(x.registeredAt.slice(0, 10)) : "") + "</td><td>" + (x.billing ? esc(x.billing) : "<span class=muted>none</span>") + "</td><td>" + x.appends + (x.quota === null ? "" : " <span class=muted>/ " + x.quota + "</span>") + "</td><td>" + x.totalLeaves + "</td><td>" + x.liveTokens + "</td></tr>").join("") || "<tr><td colspan=9 class=muted>no tenants</td></tr>";
|
|
332
342
|
};
|
|
333
343
|
const loadAudit = async () => {
|
package/dist/portal.js
CHANGED
|
@@ -241,8 +241,12 @@ export function portalHandler(o) {
|
|
|
241
241
|
try {
|
|
242
242
|
if (req.method === "GET" && url.pathname === "/health")
|
|
243
243
|
return json(200, { ok: true, stripe: !!o.stripe });
|
|
244
|
+
if (req.method === "GET" && url.pathname === "/favicon.svg") {
|
|
245
|
+
res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "public, max-age=86400" });
|
|
246
|
+
return void res.end(FAVICON);
|
|
247
|
+
}
|
|
244
248
|
if (req.method === "GET" && url.pathname === "/") {
|
|
245
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-frame-options": "DENY", "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; form-action 'self'; base-uri 'none'" });
|
|
249
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-frame-options": "DENY", "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'none'" });
|
|
246
250
|
return void res.end(PORTAL_PAGE.replace("__LOG_BASE__", base.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c])));
|
|
247
251
|
}
|
|
248
252
|
// ---- Stripe's webhook: the only caller that is not a browser with a session ----
|
|
@@ -417,18 +421,23 @@ export function servePortal(o, opts) {
|
|
|
417
421
|
});
|
|
418
422
|
}
|
|
419
423
|
// ---- the page: one file, no framework, no outside requests ----
|
|
424
|
+
const FAVICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#b45309"/><circle cx="32" cy="32" r="19" fill="none" stroke="#fff" stroke-width="4.5"/><path d="M22 33.5l7 6.5 13-15" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
420
425
|
const PORTAL_PAGE = `<!doctype html>
|
|
421
426
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
422
427
|
<title>agent-custody</title>
|
|
428
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
429
|
+
<script>(() => { try { const t = localStorage.getItem("agent-custody-theme"); if (t === "dark" || t === "light") document.documentElement.dataset.theme = t; } catch {} })();</script>
|
|
423
430
|
<style>
|
|
424
431
|
:root { color-scheme: light dark; --ink: #1b2430; --ink2: #5b6b7a; --line: #d7dfe5; --bg: #f5f7f9; --panel: #ffffff; --accent: #b45309; --accent-bg: #fbf1dc; --warn: #8a5a00; --bad: #b3261e; --ok: #1f7a4d; --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
|
|
425
|
-
@media (prefers-color-scheme: dark) { :root { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #f59e0b; --accent-bg: #2d2412; --warn: #e2b862; --bad: #ff8a80; --ok: #6fd39a; } }
|
|
432
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #f59e0b; --accent-bg: #2d2412; --warn: #e2b862; --bad: #ff8a80; --ok: #6fd39a; } }
|
|
433
|
+
:root[data-theme="dark"] { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #f59e0b; --accent-bg: #2d2412; --warn: #e2b862; --bad: #ff8a80; --ok: #6fd39a; }
|
|
426
434
|
* { box-sizing: border-box; }
|
|
427
435
|
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
428
436
|
.top { display: flex; align-items: center; gap: 1rem; padding: .7rem 1.25rem; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
429
437
|
.brand { font-weight: 700; letter-spacing: .04em; display: inline-flex; align-items: center; gap: .5rem; color: var(--ink); text-decoration: none; } .brand svg { width: 20px; height: 20px; display: block; } .brand rect { fill: var(--accent); }
|
|
430
438
|
.top .links { margin-left: auto; display: flex; gap: .9rem; font-size: .9rem; } .top .links a { color: var(--accent); text-decoration: none; }
|
|
431
439
|
.top .who { color: var(--ink2); font-size: .9rem; margin-left: .5rem; }
|
|
440
|
+
button.theme { background: transparent; color: var(--ink2); border: 1px solid var(--line); padding: .25rem .6rem; font-size: .8rem; border-radius: 4px; } button.theme:hover { color: var(--ink); }
|
|
432
441
|
.pill { display: inline-block; padding: .05rem .5rem; border-radius: 999px; font: 600 .72rem/1.6 var(--mono); letter-spacing: .06em; text-transform: uppercase; background: var(--accent-bg); color: var(--accent); }
|
|
433
442
|
.layout { display: grid; grid-template-columns: 15rem 1fr; min-height: calc(100vh - 3.3rem); }
|
|
434
443
|
nav { border-right: 1px solid var(--line); background: var(--panel); padding: 1rem 0; }
|
|
@@ -475,7 +484,7 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
475
484
|
[hidden] { display: none !important; }
|
|
476
485
|
@media (max-width: 48rem) { .layout { grid-template-columns: 1fr; } nav { display: flex; flex-wrap: wrap; padding: .3rem; border-right: 0; border-bottom: 1px solid var(--line); } nav .group { display: none; } nav a { border-left: 0; border-bottom: 3px solid transparent; } nav a.on { border-bottom-color: var(--accent); } }
|
|
477
486
|
</style>
|
|
478
|
-
<div class="top"><a class="brand" href="https://agent-custody.dev/"><svg viewBox="0 0 64 64" aria-hidden="true"><rect width="64" height="64" rx="14"/><circle cx="32" cy="32" r="19" fill="none" stroke="#fff" stroke-width="4.5"/><path d="M22 33.5l7 6.5 13-15" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></svg>agent-custody</a><span id="tenantTag" class="pill" hidden></span><span id="planTag" class="pill" hidden></span><span class="links"><a href="https://agent-custody.dev/guide/getting-started">Guide</a><a href="https://docs.agent-custody.dev/reference/">Docs</a><a href="https://agent-custody.dev/verify">Verify a receipt</a></span><span class="who" id="who"></span></div>
|
|
487
|
+
<div class="top"><a class="brand" href="https://agent-custody.dev/"><svg viewBox="0 0 64 64" aria-hidden="true"><rect width="64" height="64" rx="14"/><circle cx="32" cy="32" r="19" fill="none" stroke="#fff" stroke-width="4.5"/><path d="M22 33.5l7 6.5 13-15" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></svg>agent-custody</a><span id="tenantTag" class="pill" hidden></span><span id="planTag" class="pill" hidden></span><span class="links"><a href="https://agent-custody.dev/guide/getting-started">Guide</a><a href="https://docs.agent-custody.dev/reference/">Docs</a><a href="https://agent-custody.dev/verify">Verify a receipt</a></span><span class="who" id="who"></span><button class="theme" id="themeToggle" type="button">Dark mode</button></div>
|
|
479
488
|
<section id="auth" class="auth" hidden>
|
|
480
489
|
<h1 id="authTitle">Sign in</h1>
|
|
481
490
|
<form id="authForm">
|
|
@@ -598,17 +607,22 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
598
607
|
} catch (err) { $("authMsg").className = "msg err"; $("authMsg").textContent = err.message; }
|
|
599
608
|
};
|
|
600
609
|
$("copyFirst").onclick = () => navigator.clipboard.writeText($("firstToken").textContent);
|
|
610
|
+
// Light or dark by choice, kept in this browser; unset, the page follows the system. The button says where it is going.
|
|
611
|
+
const themeNow = () => document.documentElement.dataset.theme || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
612
|
+
const themeLabel = () => { $("themeToggle").textContent = themeNow() === "dark" ? "Light mode" : "Dark mode"; };
|
|
613
|
+
$("themeToggle").onclick = () => { const next = themeNow() === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = next; try { localStorage.setItem("agent-custody-theme", next); } catch {} themeLabel(); };
|
|
614
|
+
themeLabel();
|
|
601
615
|
const snip = (text) => "<div class=snip><pre>" + esc(text) + "</pre><button type=button class=quiet data-copy>Copy</button></div>";
|
|
602
616
|
const step = (n, title, body) => "<div class=step><span class=n>" + n + "</span><div><h3>" + title + "</h3>" + body + "</div></div>";
|
|
603
617
|
const renderSetup = (el, s, atRegistration) => {
|
|
604
618
|
el.innerHTML =
|
|
605
|
-
step(1, "Keep the key where your gateway runs", (atRegistration ? "<p>The key above is shown once; we keep only its hash. Put it in the environment of the machine that runs your gateway or SDK:</p>" : "<p>Your key was shown once at registration. If it is gone, mint another under <a href
|
|
619
|
+
step(1, "Keep the key where your gateway runs", (atRegistration ? "<p>The key above is shown once; we keep only its hash. Put it in the environment of the machine that runs your gateway or SDK:</p>" : "<p>Your key was shown once at registration. If it is gone, mint another under <a href=\\"#keys\\" data-view=\\"keys\\">API keys</a>. It lives in the environment of the machine that runs your gateway or SDK:</p>") + snip(s.env)) +
|
|
606
620
|
step(2, "Point your gateway or SDK at your log", "<p>Add this to <code>gateway.json</code> or <code>sdk.json</code>. <code>hashOnly</code> means this log receives the hash of each receipt and never the receipt.</p>" + snip(s.config)) +
|
|
607
|
-
step(3, "Send the first receipt", "<p>Run your agent through the gateway once. The <a href
|
|
621
|
+
step(3, "Send the first receipt", "<p>Run your agent through the gateway once. The <a href=\\"#overview\\" data-view=\\"overview\\">Overview</a> shows the append within seconds, and the first signed checkpoint follows within minutes. New to the gateway? <a href=\\"https://agent-custody.dev/guide/getting-started\\">Getting started</a> takes ten minutes.</p>") +
|
|
608
622
|
step(4, "Hand this to whoever verifies your receipts", "<p>Both commands fetch this log's published keys and pin them; <code>--log-id</code> makes sure the tree heads are this log's.</p>" + snip(s.verify) + snip(s.audit)) +
|
|
609
623
|
step(5, "Take your data, any time", "<p>Every leaf hash, the signed head, the keys, the checkpoints, your usage, and the actions taken on your tenant, checked against each other and written as a log copy the verifier reads offline.</p>" + snip(s.export)) +
|
|
610
624
|
"<h2>Your log's addresses</h2><div class=panel><table class=addr><tbody>" + [["Your log", s.log], ["Log id on tree heads", s.logId], ["Your checkpoints", s.checkpoints || "published after your first append"], ["The log's keys", s.keys + (s.keyid ? " (current keyid " + s.keyid.slice(0, 12) + "…)" : "")]].map(([k, v]) => "<tr><th>" + esc(k) + "</th><td class=mono>" + esc(v) + "</td></tr>").join("") + "</tbody></table></div>" +
|
|
611
|
-
"<h2>What this log does not do</h2><p class=muted>Hold receipt contents; forge a receipt, since your gateway key signs those; or, today, countersign with a second independent witness. <a href
|
|
625
|
+
"<h2>What this log does not do</h2><p class=muted>Hold receipt contents; forge a receipt, since your gateway key signs those; or, today, countersign with a second independent witness. <a href=\\"https://agent-custody.dev/receipts/#what-a-receipt-proves-and-what-it-does-not\\">What a receipt proves and what it does not.</a></p>";
|
|
612
626
|
};
|
|
613
627
|
document.addEventListener("click", (e) => {
|
|
614
628
|
const b = e.target.closest("button[data-copy]"); if (b) { navigator.clipboard.writeText(b.previousElementSibling.textContent).then(() => { b.textContent = "Copied"; setTimeout(() => { b.textContent = "Copy"; }, 1500); }); return; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-custody/receipts",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "Chain of custody for AI agents: signed, independently verifiable receipts for tool calls. MCP gateway + Cedar policy + Merkle transparency log",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|