@agent-custody/receipts 0.6.2 → 0.6.4
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/cli.js +2 -2
- package/dist/log-admin.d.ts +31 -1
- package/dist/log-admin.js +44 -2
- package/dist/portal.d.ts +7 -0
- package/dist/portal.js +9 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { connectSigner, fetchLogKeys, localSigner, serveSigner } from "./signer.
|
|
|
14
14
|
import { fetchWitnessKeys, Witness } from "./witness.js";
|
|
15
15
|
import { checkLog, formatLogCheck } from "./log-check.js";
|
|
16
16
|
import { serveHttp } from "./gateway-http.js";
|
|
17
|
-
import { servePortal } from "./portal.js";
|
|
17
|
+
import { PortalStore, servePortal } from "./portal.js";
|
|
18
18
|
import { exportLog, formatExport } from "./log-export.js";
|
|
19
19
|
import { CheckpointPublisher, fileResolver } from "./log-sink.js";
|
|
20
20
|
import { createRequire } from "node:module";
|
|
@@ -407,7 +407,7 @@ async function main(argv) {
|
|
|
407
407
|
const adminToken = process.env[values["admin-token-env"]];
|
|
408
408
|
if (!adminToken)
|
|
409
409
|
throw new Error(`log: environment variable ${values["admin-token-env"]} is not set`);
|
|
410
|
-
admin = { tenancy, token: adminToken, ...(values["public-url"] ? { publicUrl: values["public-url"] } : {}), ...(values["checkpoints-url"] ? { checkpointsUrl: values["checkpoints-url"] } : {}) };
|
|
410
|
+
admin = { tenancy, token: adminToken, portal: new PortalStore(client), ...(values["public-url"] ? { publicUrl: values["public-url"] } : {}), ...(values["checkpoints-url"] ? { checkpointsUrl: values["checkpoints-url"] } : {}) };
|
|
411
411
|
}
|
|
412
412
|
where = `store=postgres default-log=${(await tenancy.tenant("default"))?.logId} ${token ? "environment token accepted for the default log; " : ""}tokens from the database`;
|
|
413
413
|
}
|
package/dist/log-admin.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import { type PostgresTenancy } from "./log-store.ts";
|
|
2
|
+
import { type PostgresTenancy, type Plan } from "./log-store.ts";
|
|
3
|
+
import type { PortalStore } from "./portal.ts";
|
|
3
4
|
export interface AdminOptions {
|
|
4
5
|
tenancy: PostgresTenancy;
|
|
5
6
|
/** the admin token; every /admin route needs it as a bearer */
|
|
@@ -12,6 +13,8 @@ export interface AdminOptions {
|
|
|
12
13
|
keyid?: string;
|
|
13
14
|
/** key the failure throttle by X-Forwarded-For's first address; only behind a proxy you run */
|
|
14
15
|
trustProxy?: boolean;
|
|
16
|
+
/** the portal's tables on the same database, read only, so the registrations list can say who signed up */
|
|
17
|
+
portal?: PortalStore;
|
|
15
18
|
}
|
|
16
19
|
/** The welcome sheet as text, the same one deploy/onboard-tenant.sh prints. */
|
|
17
20
|
export declare function welcomeSheet(o: {
|
|
@@ -33,5 +36,32 @@ export declare function welcomeSheet(o: {
|
|
|
33
36
|
* POST /admin/tenants/:id/tokens/:prefix/revoke { revoked }
|
|
34
37
|
* GET /admin/usage?month=YYYY-MM { month, tenants: [{ id, logId, appends, totalLeaves, liveTokens, disabled }] }
|
|
35
38
|
* GET /admin/usage.csv?month=YYYY-MM the same as CSV, for an invoice
|
|
39
|
+
* GET /admin/registrations?month=YYYY-MM { month, rows: [{ tenant, logId, plan, email, registeredAt, billing, appends, quota, totalLeaves, liveTokens, disabled }], totals }
|
|
40
|
+
* GET /admin/registrations.csv?month=YYYY-MM the same as CSV
|
|
36
41
|
*/
|
|
42
|
+
/** One row per tenant: who registered it through the portal (null when it was onboarded by script), its plan and billing
|
|
43
|
+
* state, appends in the month asked for, and leaves in total; plus the totals across tenants. */
|
|
44
|
+
export declare function registrations(tenancy: PostgresTenancy, portal: PortalStore | undefined, month: string): Promise<{
|
|
45
|
+
month: string;
|
|
46
|
+
rows: RegistrationRow[];
|
|
47
|
+
totals: {
|
|
48
|
+
tenants: number;
|
|
49
|
+
registered: number;
|
|
50
|
+
appends: number;
|
|
51
|
+
totalLeaves: number;
|
|
52
|
+
};
|
|
53
|
+
}>;
|
|
54
|
+
export interface RegistrationRow {
|
|
55
|
+
tenant: string;
|
|
56
|
+
logId: string;
|
|
57
|
+
plan: Plan;
|
|
58
|
+
email: string | null;
|
|
59
|
+
registeredAt: string | null;
|
|
60
|
+
billing: string | null;
|
|
61
|
+
appends: number;
|
|
62
|
+
quota: number | null;
|
|
63
|
+
totalLeaves: number;
|
|
64
|
+
liveTokens: number;
|
|
65
|
+
disabled: boolean;
|
|
66
|
+
}
|
|
37
67
|
export declare function adminRoutes(opts: AdminOptions): (req: IncomingMessage, res: ServerResponse, url: URL) => Promise<boolean>;
|
package/dist/log-admin.js
CHANGED
|
@@ -57,7 +57,23 @@ export function welcomeSheet(o) {
|
|
|
57
57
|
* POST /admin/tenants/:id/tokens/:prefix/revoke { revoked }
|
|
58
58
|
* GET /admin/usage?month=YYYY-MM { month, tenants: [{ id, logId, appends, totalLeaves, liveTokens, disabled }] }
|
|
59
59
|
* GET /admin/usage.csv?month=YYYY-MM the same as CSV, for an invoice
|
|
60
|
+
* GET /admin/registrations?month=YYYY-MM { month, rows: [{ tenant, logId, plan, email, registeredAt, billing, appends, quota, totalLeaves, liveTokens, disabled }], totals }
|
|
61
|
+
* GET /admin/registrations.csv?month=YYYY-MM the same as CSV
|
|
60
62
|
*/
|
|
63
|
+
/** One row per tenant: who registered it through the portal (null when it was onboarded by script), its plan and billing
|
|
64
|
+
* state, appends in the month asked for, and leaves in total; plus the totals across tenants. */
|
|
65
|
+
export async function registrations(tenancy, portal, month) {
|
|
66
|
+
const usage = await tenancy.usage(month);
|
|
67
|
+
const who = new Map();
|
|
68
|
+
for (const r of portal ? await portal.registrations() : [])
|
|
69
|
+
if (!who.has(r.tenantId))
|
|
70
|
+
who.set(r.tenantId, r);
|
|
71
|
+
const rows = usage.tenants.map((t) => {
|
|
72
|
+
const w = who.get(t.id);
|
|
73
|
+
return { tenant: t.id, logId: t.logId, plan: t.plan, email: w?.email ?? null, registeredAt: w?.registeredAt ?? null, billing: w?.billing ?? null, appends: t.appends, quota: t.quota, totalLeaves: t.totalLeaves, liveTokens: t.liveTokens, disabled: t.disabled };
|
|
74
|
+
});
|
|
75
|
+
return { month: usage.month, rows, totals: { tenants: rows.length, registered: rows.filter((r) => r.email).length, appends: rows.reduce((n, r) => n + r.appends, 0), totalLeaves: rows.reduce((n, r) => n + r.totalLeaves, 0) } };
|
|
76
|
+
}
|
|
61
77
|
export function adminRoutes(opts) {
|
|
62
78
|
// Five wrong tokens from one address, then one more a minute: enough to stop guessing, not enough to lock out a typo.
|
|
63
79
|
const failures = new RateLimiter({ perSecond: 1 / 60, burst: 5 });
|
|
@@ -118,6 +134,16 @@ export function adminRoutes(opts) {
|
|
|
118
134
|
res.writeHead(200, { "content-type": "text/csv; charset=utf-8", "content-disposition": `attachment; filename="agent-custody-usage-${u.month}.csv"`, "cache-control": "no-store" });
|
|
119
135
|
res.end(csv);
|
|
120
136
|
}
|
|
137
|
+
else if (req.method === "GET" && parts.length === 2 && parts[1] === "registrations") {
|
|
138
|
+
json(200, await registrations(t, opts.portal, month));
|
|
139
|
+
}
|
|
140
|
+
else if (req.method === "GET" && parts.length === 2 && parts[1] === "registrations.csv") {
|
|
141
|
+
const r = await registrations(t, opts.portal, month);
|
|
142
|
+
const cell = (v) => (v === null ? "" : /[",\n]/.test(String(v)) ? `"${String(v).replace(/"/g, '""')}"` : String(v));
|
|
143
|
+
const csv = ["month,tenant,log_id,plan,email,registered_at,billing,appends,quota,total_leaves,live_tokens,disabled", ...r.rows.map((x) => [r.month, x.tenant, x.logId, x.plan, x.email, x.registeredAt, x.billing, x.appends, x.quota, x.totalLeaves, x.liveTokens, x.disabled].map(cell).join(","))].join("\n") + "\n";
|
|
144
|
+
res.writeHead(200, { "content-type": "text/csv; charset=utf-8", "content-disposition": `attachment; filename="agent-custody-registrations-${r.month}.csv"`, "cache-control": "no-store" });
|
|
145
|
+
res.end(csv);
|
|
146
|
+
}
|
|
121
147
|
else if (req.method === "GET" && parts.length === 2 && parts[1] === "audit") {
|
|
122
148
|
const tenant = url.searchParams.get("tenant");
|
|
123
149
|
const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : 200;
|
|
@@ -200,11 +226,18 @@ const ADMIN_PAGE = `<!doctype html>
|
|
|
200
226
|
.muted { color: var(--ink2); } .err { color: #b3261e; } .ok { color: var(--accent); }
|
|
201
227
|
.tok { font-family: var(--mono); font-size: 1.05rem; word-break: break-all; user-select: all; }
|
|
202
228
|
[hidden] { display: none !important; }
|
|
229
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .6rem; margin: 0 0 1rem; }
|
|
230
|
+
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; padding: .6rem .8rem; }
|
|
231
|
+
.stat b { display: block; font-size: 1.4rem; font-variant-numeric: tabular-nums; } .stat span { color: var(--ink2); font-size: .8rem; }
|
|
203
232
|
</style>
|
|
204
233
|
<main>
|
|
205
234
|
<h1>Log admin</h1>
|
|
206
235
|
<p class="sub" id="where">Tenants and tokens on this log.</p>
|
|
207
236
|
<section id="app">
|
|
237
|
+
<h2>Registrations</h2>
|
|
238
|
+
<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>
|
|
239
|
+
<div class="stats"><div class="stat"><b id="sTenants">–</b><span>tenants</span></div><div class="stat"><b id="sRegistered">–</b><span>registered through the portal</span></div><div class="stat"><b id="sAppends">–</b><span>appends this month</span></div><div class="stat"><b id="sLeaves">–</b><span>leaves in total</span></div></div>
|
|
240
|
+
<table><thead><tr><th>email</th><th>tenant</th><th>plan</th><th>registered</th><th>billing</th><th>appends this month</th><th>leaves in total</th><th>live tokens</th></tr></thead><tbody id="regs"></tbody></table>
|
|
208
241
|
<h2>Tenants</h2>
|
|
209
242
|
<table><thead><tr><th>tenant</th><th>log id</th><th>plan</th><th>live tokens</th><th>created</th><th></th></tr></thead><tbody id="tenants"></tbody></table>
|
|
210
243
|
<h2>New tenant</h2>
|
|
@@ -263,6 +296,7 @@ const ADMIN_PAGE = `<!doctype html>
|
|
|
263
296
|
try {
|
|
264
297
|
const info = await api("GET", "/admin/info");
|
|
265
298
|
$("where").textContent = (info.publicUrl || location.origin) + " · keyid " + (info.keyid ? info.keyid.slice(0, 12) : "?") + (info.checkpointsUrl ? " · checkpoints at " + info.checkpointsUrl : "");
|
|
299
|
+
await loadRegs();
|
|
266
300
|
await loadTenants();
|
|
267
301
|
await loadUsage();
|
|
268
302
|
await loadAudit();
|
|
@@ -287,15 +321,23 @@ const ADMIN_PAGE = `<!doctype html>
|
|
|
287
321
|
$("csv").href = "/admin/usage.csv?month=" + encodeURIComponent(month);
|
|
288
322
|
$("usage").innerHTML = u.tenants.map((t) => "<tr><td><code>" + esc(t.id) + "</code>" + (t.disabled ? " <span class=muted>disabled</span>" : "") + "</td><td><code>" + esc(t.logId) + "</code></td><td>" + esc(t.plan) + "</td><td>" + t.appends + "</td><td>" + (t.quota === null ? "none" : t.quota) + "</td><td>" + t.totalLeaves + "</td><td>" + t.liveTokens + "</td></tr>").join("") || "<tr><td colspan=7 class=muted>no tenants</td></tr>";
|
|
289
323
|
};
|
|
324
|
+
const loadRegs = async () => {
|
|
325
|
+
const month = $("rmonth").value || new Date().toISOString().slice(0, 7);
|
|
326
|
+
const r = await api("GET", "/admin/registrations?month=" + encodeURIComponent(month));
|
|
327
|
+
$("rcsv").href = "/admin/registrations.csv?month=" + encodeURIComponent(month);
|
|
328
|
+
$("sTenants").textContent = r.totals.tenants; $("sRegistered").textContent = r.totals.registered; $("sAppends").textContent = r.totals.appends; $("sLeaves").textContent = r.totals.totalLeaves;
|
|
329
|
+
$("regs").innerHTML = r.rows.map((x) => "<tr><td>" + (x.email ? esc(x.email) : "<span class=muted>onboarded by script</span>") + "</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=8 class=muted>no tenants</td></tr>";
|
|
330
|
+
};
|
|
290
331
|
const loadAudit = async () => {
|
|
291
332
|
const a = await api("GET", "/admin/audit?limit=100");
|
|
292
333
|
$("audit").innerHTML = a.entries.map((e) => "<tr><td>" + esc(e.at.replace("T", " ").slice(0, 19)) + "</td><td><code>" + esc(e.actor) + "</code></td><td>" + esc(e.action) + "</td><td><code>" + esc(e.tenantId || "") + "</code></td><td class=muted>" + esc(Object.entries(e.detail).map(([k, v]) => k + "=" + v).join(" ")) + "</td></tr>").join("") || "<tr><td colspan=5 class=muted>nothing yet</td></tr>";
|
|
293
334
|
};
|
|
294
335
|
$("loadUsage").onclick = () => loadUsage().catch((e) => say(e.message, "err"));
|
|
295
|
-
$("
|
|
336
|
+
$("loadRegs").onclick = () => loadRegs().catch((e) => say(e.message, "err"));
|
|
337
|
+
$("month").value = $("rmonth").value = new Date().toISOString().slice(0, 7);
|
|
296
338
|
document.addEventListener("change", async (e) => {
|
|
297
339
|
const s = e.target.closest("select[data-plan]"); if (!s) return;
|
|
298
|
-
try { await api("POST", "/admin/tenants/" + encodeURIComponent(s.dataset.plan) + "/plan", { plan: s.value }); say("plan of " + s.dataset.plan + " set to " + s.value, "ok"); await loadUsage(); await loadAudit(); } catch (err) { say(err.message, "err"); await loadTenants(); }
|
|
340
|
+
try { await api("POST", "/admin/tenants/" + encodeURIComponent(s.dataset.plan) + "/plan", { plan: s.value }); say("plan of " + s.dataset.plan + " set to " + s.value, "ok"); await loadRegs(); await loadUsage(); await loadAudit(); } catch (err) { say(err.message, "err"); await loadTenants(); }
|
|
299
341
|
});
|
|
300
342
|
document.addEventListener("click", async (e) => {
|
|
301
343
|
const b = e.target.closest("button"); if (!b) return;
|
package/dist/portal.d.ts
CHANGED
|
@@ -57,6 +57,13 @@ export declare class PortalStore {
|
|
|
57
57
|
subscriptionId: string | null;
|
|
58
58
|
status: string;
|
|
59
59
|
} | null>;
|
|
60
|
+
/** Every portal registration with its tenant and billing state, oldest first: what the operator's admin page lists. */
|
|
61
|
+
registrations(): Promise<{
|
|
62
|
+
tenantId: string;
|
|
63
|
+
email: string;
|
|
64
|
+
registeredAt: string;
|
|
65
|
+
billing: string | null;
|
|
66
|
+
}[]>;
|
|
60
67
|
tenantBySubscription(subscriptionId: string): Promise<string | null>;
|
|
61
68
|
}
|
|
62
69
|
export declare function signSession(secret: string, userId: string, ttlMs?: number): string;
|
package/dist/portal.js
CHANGED
|
@@ -95,6 +95,12 @@ export class PortalStore {
|
|
|
95
95
|
const r = rows[0];
|
|
96
96
|
return r ? { customerId: r.customer_id ? String(r.customer_id) : null, subscriptionId: r.subscription_id ? String(r.subscription_id) : null, status: String(r.status) } : null;
|
|
97
97
|
}
|
|
98
|
+
/** Every portal registration with its tenant and billing state, oldest first: what the operator's admin page lists. */
|
|
99
|
+
async registrations() {
|
|
100
|
+
await this.init();
|
|
101
|
+
const rows = (await this.client.query(`SELECT m.tenant_id, u.email, u.created_at, b.status FROM ${this.p}members m JOIN ${this.p}users u ON u.id = m.user_id LEFT JOIN ${this.p}billing b ON b.tenant_id = m.tenant_id ORDER BY u.created_at, m.tenant_id`)).rows;
|
|
102
|
+
return rows.map((r) => ({ tenantId: String(r.tenant_id), email: String(r.email), registeredAt: new Date(r.created_at).toISOString(), billing: r.status ? String(r.status) : null }));
|
|
103
|
+
}
|
|
98
104
|
async tenantBySubscription(subscriptionId) {
|
|
99
105
|
await this.init();
|
|
100
106
|
const rows = (await this.client.query(`SELECT tenant_id FROM ${this.p}billing WHERE subscription_id = $1`, [subscriptionId])).rows;
|
|
@@ -365,7 +371,8 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
365
371
|
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
366
372
|
.top { display: flex; align-items: center; gap: 1rem; padding: .7rem 1.25rem; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
367
373
|
.brand { font-weight: 700; letter-spacing: .04em; } .brand b { color: var(--accent); }
|
|
368
|
-
.top .
|
|
374
|
+
.top .links { margin-left: auto; display: flex; gap: .9rem; font-size: .9rem; } .top .links a { color: var(--accent); text-decoration: none; }
|
|
375
|
+
.top .who { color: var(--ink2); font-size: .9rem; margin-left: .5rem; }
|
|
369
376
|
.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); }
|
|
370
377
|
.layout { display: grid; grid-template-columns: 15rem 1fr; min-height: calc(100vh - 3.3rem); }
|
|
371
378
|
nav { border-right: 1px solid var(--line); background: var(--panel); padding: 1rem 0; }
|
|
@@ -404,7 +411,7 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
404
411
|
[hidden] { display: none !important; }
|
|
405
412
|
@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); } }
|
|
406
413
|
</style>
|
|
407
|
-
<div class="top"><span class="brand"><b>◆</b> agent-custody</span><span id="tenantTag" class="pill" hidden></span><span id="planTag" class="pill" hidden></span><span class="who" id="who"></span></div>
|
|
414
|
+
<div class="top"><span class="brand"><b>◆</b> agent-custody</span><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>
|
|
408
415
|
<section id="auth" class="auth" hidden>
|
|
409
416
|
<h1 id="authTitle">Sign in</h1>
|
|
410
417
|
<form id="authForm">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-custody/receipts",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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": {
|