@agent-custody/receipts 0.6.7 → 0.6.8
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 +11 -2
- package/dist/index.d.ts +1 -1
- package/dist/portal.d.ts +12 -0
- package/dist/portal.js +78 -10
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -267,9 +267,18 @@ async function main(argv) {
|
|
|
267
267
|
return 0;
|
|
268
268
|
}
|
|
269
269
|
case "portal": {
|
|
270
|
-
const { values } = parseArgs({ args: rest, options: { "db-env": { type: "string" }, "secret-env": { type: "string" }, "public-url": { type: "string" }, "checkpoints-url": { type: "string" }, "portal-url": { type: "string" }, port: { type: "string", default: "8792" }, host: { type: "string", default: "127.0.0.1" }, "stripe-key-env": { type: "string" }, "stripe-webhook-env": { type: "string" }, "stripe-price-team": { type: "string" }, "trust-proxy": { type: "boolean", default: false } } });
|
|
270
|
+
const { values } = parseArgs({ args: rest, options: { "db-env": { type: "string" }, "secret-env": { type: "string" }, "public-url": { type: "string" }, "checkpoints-url": { type: "string" }, "portal-url": { type: "string" }, port: { type: "string", default: "8792" }, host: { type: "string", default: "127.0.0.1" }, "stripe-key-env": { type: "string" }, "stripe-webhook-env": { type: "string" }, "stripe-price-team": { type: "string" }, "mail-key-env": { type: "string" }, "mail-from": { type: "string" }, "mail-notify": { type: "string" }, "trust-proxy": { type: "boolean", default: false } } });
|
|
271
271
|
if (!values["db-env"] || !values["secret-env"] || !values["public-url"])
|
|
272
272
|
throw new Error("portal needs --db-env, --secret-env, and --public-url");
|
|
273
|
+
let mail;
|
|
274
|
+
if (values["mail-key-env"] || values["mail-from"]) {
|
|
275
|
+
if (!values["mail-key-env"] || !values["mail-from"])
|
|
276
|
+
throw new Error("mail needs both --mail-key-env and --mail-from");
|
|
277
|
+
const apiKey = process.env[values["mail-key-env"]];
|
|
278
|
+
if (!apiKey)
|
|
279
|
+
throw new Error(`portal: environment variable ${values["mail-key-env"]} is not set`);
|
|
280
|
+
mail = { apiKey, from: values["mail-from"], ...(values["mail-notify"] ? { notify: values["mail-notify"] } : {}) };
|
|
281
|
+
}
|
|
273
282
|
const secret = process.env[values["secret-env"]];
|
|
274
283
|
if (!secret || secret.length < 32)
|
|
275
284
|
throw new Error(`environment variable ${values["secret-env"]} must hold a secret of at least 32 characters`);
|
|
@@ -292,7 +301,7 @@ async function main(argv) {
|
|
|
292
301
|
catch {
|
|
293
302
|
// the log may not be reachable from here at start; the sheet then omits the keyid
|
|
294
303
|
}
|
|
295
|
-
const running = await servePortal({ tenancy, client, secret, publicUrl: values["public-url"], ...(values["checkpoints-url"] ? { checkpointsUrl: values["checkpoints-url"] } : {}), ...(values["portal-url"] ? { portalUrl: values["portal-url"] } : {}), ...(keyid ? { keyid } : {}), ...(stripe ? { stripe } : {}), trustProxy: values["trust-proxy"] }, { port: Number(values.port), host: values.host });
|
|
304
|
+
const running = await servePortal({ tenancy, client, secret, publicUrl: values["public-url"], ...(values["checkpoints-url"] ? { checkpointsUrl: values["checkpoints-url"] } : {}), ...(values["portal-url"] ? { portalUrl: values["portal-url"] } : {}), ...(keyid ? { keyid } : {}), ...(stripe ? { stripe } : {}), ...(mail ? { mail } : {}), trustProxy: values["trust-proxy"] }, { port: Number(values.port), host: values.host });
|
|
296
305
|
console.error(`agent-custody portal: ${running.url} log=${values["public-url"]} billing=${stripe ? "stripe" : "off"}${values["trust-proxy"] ? " trust-proxy" : ""}`);
|
|
297
306
|
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
298
307
|
await running.close();
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { openExporter, otlpExporter, spanFor } from "./otel.ts";
|
|
|
6
6
|
export { hecEvent, splunkExporter } from "./splunk.ts";
|
|
7
7
|
export { GRANT_HEADER, grantHeader, parseGrantHeader, serveHttp } from "./gateway-http.ts";
|
|
8
8
|
export { PortalStore, portalHandler, readSession, servePortal, signSession, stripeRequest, verifyStripeSignature } from "./portal.ts";
|
|
9
|
-
export type { PortalOptions, PortalUser, RunningPortal, StripeOptions } from "./portal.ts";
|
|
9
|
+
export type { MailOptions, PortalOptions, PortalUser, Profile, RunningPortal, StripeOptions } from "./portal.ts";
|
|
10
10
|
export type { HttpGatewayOptions, RunningHttpGateway } from "./gateway-http.ts";
|
|
11
11
|
export { exportLog, formatExport } from "./log-export.ts";
|
|
12
12
|
export type { ExportOptions, ExportResult } from "./log-export.ts";
|
package/dist/portal.d.ts
CHANGED
|
@@ -7,6 +7,17 @@ export interface StripeOptions {
|
|
|
7
7
|
priceTeam: string;
|
|
8
8
|
fetch?: typeof fetch;
|
|
9
9
|
}
|
|
10
|
+
/** Outbound mail over an HTTP API in Resend's shape (`POST /emails` with a bearer key). One message at registration; never a secret. */
|
|
11
|
+
export interface MailOptions {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
/** the sender, an address on a domain the provider has verified */
|
|
14
|
+
from: string;
|
|
15
|
+
/** an operator address that gets a note per registration, with the contact details */
|
|
16
|
+
notify?: string;
|
|
17
|
+
/** the endpoint; Resend's by default */
|
|
18
|
+
url?: string;
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
10
21
|
export interface PortalOptions {
|
|
11
22
|
tenancy: PostgresTenancy;
|
|
12
23
|
/** the Postgres client the tenancy uses; the portal's own tables live beside the log's */
|
|
@@ -21,6 +32,7 @@ export interface PortalOptions {
|
|
|
21
32
|
/** the portal's own public URL, for Stripe's return addresses */
|
|
22
33
|
portalUrl?: string;
|
|
23
34
|
stripe?: StripeOptions;
|
|
35
|
+
mail?: MailOptions;
|
|
24
36
|
/** key throttles by X-Forwarded-For; only behind a proxy you run. Also marks cookies Secure. */
|
|
25
37
|
trustProxy?: boolean;
|
|
26
38
|
/** table prefix; default portal_ */
|
package/dist/portal.js
CHANGED
|
@@ -173,7 +173,46 @@ export function portalHandler(o) {
|
|
|
173
173
|
const base = o.publicUrl.endsWith("/") ? o.publicUrl : `${o.publicUrl}/`;
|
|
174
174
|
const cookieName = "custody_session";
|
|
175
175
|
const sheet = (tenant, logId) => welcomeSheet({ tenant, logId, publicUrl: base, ...(o.checkpointsUrl ? { checkpointsUrl: o.checkpointsUrl } : {}), ...(o.keyid ? { keyid: o.keyid } : {}) });
|
|
176
|
+
const portalBase = (o.portalUrl ?? "http://localhost/").replace(/\/?$/, "/");
|
|
177
|
+
// Mail is best effort and off the request path: a provider outage is logged, never a failed registration.
|
|
178
|
+
const send = async (m) => {
|
|
179
|
+
if (!o.mail)
|
|
180
|
+
return;
|
|
181
|
+
const f = o.mail.fetch ?? fetch;
|
|
182
|
+
try {
|
|
183
|
+
const r = await f(o.mail.url ?? "https://api.resend.com/emails", { method: "POST", headers: { authorization: `Bearer ${o.mail.apiKey}`, "content-type": "application/json" }, body: JSON.stringify({ from: o.mail.from, to: [m.to], subject: m.subject, text: m.text }), signal: AbortSignal.timeout(10_000) });
|
|
184
|
+
if (!r.ok)
|
|
185
|
+
log(`agent-custody portal: mail to ${m.to} refused: ${r.status} ${(await r.text()).slice(0, 200)}`);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
log(`agent-custody portal: mail to ${m.to} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const welcomeMail = (to, name, tenant, logId) => send({ to, subject: `Your agent-custody log "${tenant}" is ready`, text: [
|
|
192
|
+
`Hello ${name},`, "",
|
|
193
|
+
`Your tenant "${tenant}" is live on the hosted log. Your API key was shown once when you registered and is not in this email; if it is gone, mint another under API keys at ${portalBase}.`, "",
|
|
194
|
+
"Everything below is under Setup in the dashboard whenever you need it.", "",
|
|
195
|
+
sheet(tenant, logId), "",
|
|
196
|
+
`Dashboard: ${portalBase}`, "Getting started: https://agent-custody.dev/guide/getting-started", "Questions: reply to this email.",
|
|
197
|
+
].join("\n") });
|
|
198
|
+
const notifyMail = (p) => o.mail?.notify ? send({ to: o.mail.notify, subject: `New registration: ${p.company} (${p.tenant})`, text: [`${p.name}${p.role ? `, ${p.role}` : ""} at ${p.company} registered tenant "${p.tenant}".`, "", `email ${p.email}`, `phone ${p.phone ?? "-"}`, `telegram ${p.telegram ? `@${p.telegram}` : "-"}`, "", "The Registrations section of the admin page has the same, with their usage."].join("\n") }) : Promise.resolve();
|
|
176
199
|
const exportCommand = (tenant) => `npx @agent-custody/receipts log-export --log-url ${base} --tenant ${tenant} --token-env AGENT_CUSTODY_LOG_TOKEN --out custody-export/`;
|
|
200
|
+
// The welcome sheet as data: what the page renders as numbered steps, at registration and again under Setup.
|
|
201
|
+
const setupFor = (tenant, logId) => {
|
|
202
|
+
const url = `${base}t/${tenant}/`;
|
|
203
|
+
return {
|
|
204
|
+
log: url,
|
|
205
|
+
logId,
|
|
206
|
+
checkpoints: o.checkpointsUrl ? `${o.checkpointsUrl.replace(/\/?$/, "/")}${tenant}/latest.json` : null,
|
|
207
|
+
keys: `${base}.well-known/agent-custody-log.json`,
|
|
208
|
+
keyid: o.keyid ?? null,
|
|
209
|
+
env: "export AGENT_CUSTODY_LOG_TOKEN=<the key shown at registration>",
|
|
210
|
+
config: `"log": { "url": "${url}", "tokenEnv": "AGENT_CUSTODY_LOG_TOKEN", "hashOnly": true }`,
|
|
211
|
+
verify: `npx @agent-custody/receipts verify receipts/<id>.json --issuer-key <your gateway.pub> --principal-key <your principal.pub> --log-url ${url} --log-id ${logId}`,
|
|
212
|
+
audit: `npx @agent-custody/receipts audit --older receipts/<earlier>.json --newer receipts/<later>.json --log-url ${url} --log-id ${logId}`,
|
|
213
|
+
export: exportCommand(tenant),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
177
216
|
return async (req, res) => {
|
|
178
217
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
179
218
|
const addr = clientAddress(req, o.trustProxy);
|
|
@@ -274,7 +313,9 @@ export function portalHandler(o) {
|
|
|
274
313
|
await store.addMember(user.id, tenant);
|
|
275
314
|
const minted = await o.tenancy.addToken(tenant, "first key", `portal:${email}`);
|
|
276
315
|
log(`agent-custody portal: ${email} registered tenant ${tenant}`);
|
|
277
|
-
|
|
316
|
+
void welcomeMail(email, profile.name, t.id, t.logId);
|
|
317
|
+
void notifyMail({ ...profile, email, tenant: t.id });
|
|
318
|
+
return json(200, { tenant: t.id, logId: t.logId, plan: t.plan, token: minted.token, tokenHash: minted.tokenHash.slice(0, 12), welcome: sheet(t.id, t.logId), setup: setupFor(t.id, t.logId), exportCommand: exportCommand(t.id) }, { "set-cookie": setCookie(signSession(o.secret, user.id)) });
|
|
278
319
|
}
|
|
279
320
|
if (req.method === "POST" && url.pathname === "/api/login") {
|
|
280
321
|
if (!loginFailures.take(`login:${addr}`))
|
|
@@ -320,7 +361,7 @@ export function portalHandler(o) {
|
|
|
320
361
|
const keys = (await o.tenancy.listTokens(tenantId)).map((k) => ({ label: k.label, hash: k.tokenHash.slice(0, 12), createdAt: k.createdAt, revokedAt: k.revokedAt }));
|
|
321
362
|
const audit = await o.tenancy.audit({ tenant: tenantId, limit: 50 });
|
|
322
363
|
const billing = await store.billing(tenantId);
|
|
323
|
-
return json(200, { tenant: tenantId, logId: tenant.logId, plan: q.plan, used: q.used, quota: q.quota, treeSize: size, rootHash: size ? await backend.root(size) : null, latestCheckpoint: latest ? { treeSize: latest.treeSize, signedAt: latest.signedAt } : null, months, keys, audit, billing: billing ? { status: billing.status } : null, urls: { log: `${base}t/${tenantId}/`, keys: `${base}.well-known/agent-custody-log.json`, checkpoints: o.checkpointsUrl ? `${o.checkpointsUrl.replace(/\/?$/, "/")}${tenantId}/latest.json` : null }, exportCommand: exportCommand(tenantId), welcome: sheet(tenantId, tenant.logId), stripe: !!o.stripe });
|
|
364
|
+
return json(200, { tenant: tenantId, logId: tenant.logId, plan: q.plan, used: q.used, quota: q.quota, treeSize: size, rootHash: size ? await backend.root(size) : null, latestCheckpoint: latest ? { treeSize: latest.treeSize, signedAt: latest.signedAt } : null, months, keys, audit, billing: billing ? { status: billing.status } : null, urls: { log: `${base}t/${tenantId}/`, keys: `${base}.well-known/agent-custody-log.json`, checkpoints: o.checkpointsUrl ? `${o.checkpointsUrl.replace(/\/?$/, "/")}${tenantId}/latest.json` : null }, exportCommand: exportCommand(tenantId), welcome: sheet(tenantId, tenant.logId), setup: setupFor(tenantId, tenant.logId), stripe: !!o.stripe });
|
|
324
365
|
}
|
|
325
366
|
if (req.method === "GET" && url.pathname === "/api/keys") {
|
|
326
367
|
return json(200, { keys: (await o.tenancy.listTokens(tenantId)).map((k) => ({ label: k.label, hash: k.tokenHash.slice(0, 12), createdAt: k.createdAt, revokedAt: k.revokedAt })) });
|
|
@@ -420,6 +461,13 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
420
461
|
button.quiet { background: transparent; color: var(--accent); }
|
|
421
462
|
button.link { background: none; border: 0; padding: 0; color: var(--accent); text-decoration: underline; }
|
|
422
463
|
.auth { max-width: 26rem; margin: 4rem auto; }
|
|
464
|
+
.auth.wide { max-width: 44rem; }
|
|
465
|
+
.step { display: grid; grid-template-columns: 2rem 1fr; gap: .2rem .8rem; padding: 1rem 0; border-top: 1px solid var(--line); }
|
|
466
|
+
.step .n { font: 700 .85rem/1.6 var(--mono); color: var(--accent); }
|
|
467
|
+
.step h3 { margin: 0 0 .3rem; font-size: 1rem; } .step p { margin: 0 0 .5rem; color: var(--ink2); font-size: .92rem; }
|
|
468
|
+
.snip { position: relative; margin: .4rem 0 .6rem; } .snip pre { margin: 0; padding-right: 4.5rem; font-family: var(--mono); font-size: .84rem; }
|
|
469
|
+
.snip button { position: absolute; top: .45rem; right: .45rem; padding: .2rem .6rem; font-size: .8rem; }
|
|
470
|
+
.addr th { text-align: left; font-weight: 600; padding-right: 1rem; white-space: nowrap; } .addr td { word-break: break-all; }
|
|
423
471
|
.once { border: 1px solid var(--warn); background: color-mix(in srgb, var(--warn) 10%, var(--panel)); border-radius: 6px; padding: 1rem 1.1rem; margin: 1rem 0; }
|
|
424
472
|
.tok { font-family: var(--mono); word-break: break-all; padding: .6rem; background: var(--bg); border-radius: 4px; }
|
|
425
473
|
.msg { min-height: 1.4rem; margin: .6rem 0; color: var(--ink2); } .msg.err { color: var(--bad); } .msg.ok { color: var(--ok); }
|
|
@@ -449,12 +497,12 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
449
497
|
<p class="muted"><button class="link" id="authSwap" type="button">Create an account and a tenant instead</button></p>
|
|
450
498
|
<p class="muted">The free plan is ten thousand appends a month, no card. Your gateway sends only hashes; nothing you log here can be read by us.</p>
|
|
451
499
|
</section>
|
|
452
|
-
<section id="welcome" class="auth" hidden>
|
|
500
|
+
<section id="welcome" class="auth wide" hidden>
|
|
453
501
|
<h1>Your tenant is ready</h1>
|
|
502
|
+
<p class="muted">Three steps to your first receipt in this log. Everything here is under <b>Setup</b> in the dashboard whenever you need it again.</p>
|
|
454
503
|
<div class="once"><p><b>Your first API key, shown once.</b> Put it in the environment your gateway reads as <code>AGENT_CUSTODY_LOG_TOKEN</code>. We keep only its hash.</p><p class="tok" id="firstToken"></p><button class="quiet" id="copyFirst">Copy key</button></div>
|
|
455
|
-
<
|
|
456
|
-
<
|
|
457
|
-
<button id="toDash">Go to the dashboard</button>
|
|
504
|
+
<div id="firstSetup"></div>
|
|
505
|
+
<p><button id="toDash">Go to the dashboard</button></p>
|
|
458
506
|
</section>
|
|
459
507
|
<div class="layout" id="app" hidden>
|
|
460
508
|
<nav>
|
|
@@ -462,6 +510,7 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
462
510
|
<a href="#overview" data-view="overview">Overview</a>
|
|
463
511
|
<a href="#usage" data-view="usage">Usage</a>
|
|
464
512
|
<div class="group">Configure</div>
|
|
513
|
+
<a href="#setup" data-view="setup">Setup</a>
|
|
465
514
|
<a href="#keys" data-view="keys">API keys <span class="n" id="nKeys"></span></a>
|
|
466
515
|
<a href="#billing" data-view="billing">Billing</a>
|
|
467
516
|
<a href="#export" data-view="export">Export</a>
|
|
@@ -505,8 +554,11 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
505
554
|
<h1>Export</h1>
|
|
506
555
|
<p class="muted">Everything the log holds about you, any time, with your key: every leaf hash as a log file the verifier reads offline, the signed head, the published keys, the checkpoints, your usage, and the actions taken on your tenant. It checks itself before writing.</p>
|
|
507
556
|
<pre id="exportCmd"></pre>
|
|
508
|
-
|
|
509
|
-
|
|
557
|
+
</div>
|
|
558
|
+
<div data-pane="setup" hidden>
|
|
559
|
+
<h1>Setup</h1>
|
|
560
|
+
<p class="muted">How to connect a gateway or SDK to your log, what to hand your auditors, and how to take your data. The same sheet you saw at registration.</p>
|
|
561
|
+
<div id="setupPane"></div>
|
|
510
562
|
</div>
|
|
511
563
|
</main>
|
|
512
564
|
</div>
|
|
@@ -541,11 +593,27 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
541
593
|
try {
|
|
542
594
|
if (registering) {
|
|
543
595
|
const r = await api("POST", "/api/register", { email: $("email").value, password: $("password").value, tenant: $("tenant").value, name: $("name").value, company: $("company").value, role: $("role").value, phone: $("phone").value, telegram: $("telegram").value });
|
|
544
|
-
$("firstToken").textContent = r.token; $("
|
|
596
|
+
$("firstToken").textContent = r.token; renderSetup($("firstSetup"), r.setup, true); show("welcome");
|
|
545
597
|
} else { await api("POST", "/api/login", { email: $("email").value, password: $("password").value }); await enter(); }
|
|
546
598
|
} catch (err) { $("authMsg").className = "msg err"; $("authMsg").textContent = err.message; }
|
|
547
599
|
};
|
|
548
600
|
$("copyFirst").onclick = () => navigator.clipboard.writeText($("firstToken").textContent);
|
|
601
|
+
const snip = (text) => "<div class=snip><pre>" + esc(text) + "</pre><button type=button class=quiet data-copy>Copy</button></div>";
|
|
602
|
+
const step = (n, title, body) => "<div class=step><span class=n>" + n + "</span><div><h3>" + title + "</h3>" + body + "</div></div>";
|
|
603
|
+
const renderSetup = (el, s, atRegistration) => {
|
|
604
|
+
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=\"#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
|
+
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=\"#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
|
+
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
|
+
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
|
+
"<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=\"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
|
+
};
|
|
613
|
+
document.addEventListener("click", (e) => {
|
|
614
|
+
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; }
|
|
615
|
+
const a = e.target.closest("a[data-view]"); if (a && a.closest("main")) { e.preventDefault(); view(a.dataset.view); }
|
|
616
|
+
});
|
|
549
617
|
$("toDash").onclick = () => enter();
|
|
550
618
|
$("signout").onclick = async (e) => { e.preventDefault(); await api("POST", "/api/logout", {}); location.hash = ""; show("auth"); };
|
|
551
619
|
for (const a of document.querySelectorAll("nav a[data-view]")) a.onclick = (e) => { e.preventDefault(); view(a.dataset.view); };
|
|
@@ -567,7 +635,7 @@ const PORTAL_PAGE = `<!doctype html>
|
|
|
567
635
|
$("billingPanel").innerHTML = o.plan === "free"
|
|
568
636
|
? "<p>You are on the <b>free</b> plan: ten thousand appends a month, no card.</p><p>The <b>team</b> plan is <b>$50 a month</b>: a million appends, email support within two working days, the same export and audit trail. No availability commitment yet, and the design fails closed: when the log is unreachable your gateway withholds pre-committed calls.</p>" + (o.stripe ? "<button id=upgrade>Upgrade to team, $50/month</button>" : "<p class=muted>Card payments are not switched on for this portal yet; email us and we move the plan by hand.</p>")
|
|
569
637
|
: "<p>You are on the <b>" + esc(o.plan) + "</b> plan" + (o.billing ? " (subscription " + esc(o.billing.status) + ")" : "") + ".</p>" + (o.stripe && o.billing ? "<button class=quiet id=manage>Manage billing</button>" : "");
|
|
570
|
-
$("exportCmd").textContent = o.exportCommand; $("
|
|
638
|
+
$("exportCmd").textContent = o.exportCommand; renderSetup($("setupPane"), o.setup, false);
|
|
571
639
|
const up = $("upgrade"); if (up) up.onclick = async () => { try { const r = await api("POST", "/api/checkout", {}); location.href = r.url; } catch (err) { $("billingMsg").className = "msg err"; $("billingMsg").textContent = err.message; } };
|
|
572
640
|
const mg = $("manage"); if (mg) mg.onclick = async () => { try { const r = await api("POST", "/api/billing-portal", {}); location.href = r.url; } catch (err) { $("billingMsg").className = "msg err"; $("billingMsg").textContent = err.message; } };
|
|
573
641
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-custody/receipts",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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": {
|