@agent-custody/receipts 0.6.5 → 0.6.7

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.
@@ -36,7 +36,7 @@ export declare function welcomeSheet(o: {
36
36
  * POST /admin/tenants/:id/tokens/:prefix/revoke { revoked }
37
37
  * GET /admin/usage?month=YYYY-MM { month, tenants: [{ id, logId, appends, totalLeaves, liveTokens, disabled }] }
38
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 }
39
+ * GET /admin/registrations?month=YYYY-MM { month, rows: [{ tenant, logId, plan, email, name, company, role, phone, telegram, registeredAt, billing, appends, quota, totalLeaves, liveTokens, disabled }], totals }
40
40
  * GET /admin/registrations.csv?month=YYYY-MM the same as CSV
41
41
  */
42
42
  /** One row per tenant: who registered it through the portal (null when it was onboarded by script), its plan and billing
@@ -56,6 +56,11 @@ export interface RegistrationRow {
56
56
  logId: string;
57
57
  plan: Plan;
58
58
  email: string | null;
59
+ name: string | null;
60
+ company: string | null;
61
+ role: string | null;
62
+ phone: string | null;
63
+ telegram: string | null;
59
64
  registeredAt: string | null;
60
65
  billing: string | null;
61
66
  appends: number;
package/dist/log-admin.js CHANGED
@@ -57,7 +57,7 @@ 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 }
60
+ * GET /admin/registrations?month=YYYY-MM { month, rows: [{ tenant, logId, plan, email, name, company, role, phone, telegram, registeredAt, billing, appends, quota, totalLeaves, liveTokens, disabled }], totals }
61
61
  * GET /admin/registrations.csv?month=YYYY-MM the same as CSV
62
62
  */
63
63
  /** One row per tenant: who registered it through the portal (null when it was onboarded by script), its plan and billing
@@ -70,7 +70,7 @@ export async function registrations(tenancy, portal, month) {
70
70
  who.set(r.tenantId, r);
71
71
  const rows = usage.tenants.map((t) => {
72
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 };
73
+ return { tenant: t.id, logId: t.logId, plan: t.plan, email: w?.email ?? null, name: w?.name ?? null, company: w?.company ?? null, role: w?.role ?? null, phone: w?.phone ?? null, telegram: w?.telegram ?? 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
74
  });
75
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
76
  }
@@ -140,7 +140,7 @@ export function adminRoutes(opts) {
140
140
  else if (req.method === "GET" && parts.length === 2 && parts[1] === "registrations.csv") {
141
141
  const r = await registrations(t, opts.portal, month);
142
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";
143
+ const csv = ["month,tenant,log_id,plan,email,name,company,role,phone,telegram,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.name, x.company, x.role, x.phone, x.telegram, x.registeredAt, x.billing, x.appends, x.quota, x.totalLeaves, x.liveTokens, x.disabled].map(cell).join(","))].join("\n") + "\n";
144
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
145
  res.end(csv);
146
146
  }
@@ -237,7 +237,7 @@ const ADMIN_PAGE = `<!doctype html>
237
237
  <h2>Registrations</h2>
238
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
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>
240
+ <table><thead><tr><th>who</th><th>reach</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>
241
241
  <h2>Tenants</h2>
242
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>
243
243
  <h2>New tenant</h2>
@@ -326,7 +326,9 @@ const ADMIN_PAGE = `<!doctype html>
326
326
  const r = await api("GET", "/admin/registrations?month=" + encodeURIComponent(month));
327
327
  $("rcsv").href = "/admin/registrations.csv?month=" + encodeURIComponent(month);
328
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>";
329
+ 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=\"mailto:" + esc(x.email) + "\">" + esc(x.email) + "</a>" : "", x.phone ? esc(x.phone) : "", x.telegram ? "telegram @" + esc(x.telegram) : ""].filter(Boolean).join("<br>");
331
+ $("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>";
330
332
  };
331
333
  const loadAudit = async () => {
332
334
  const a = await api("GET", "/admin/audit?limit=100");
package/dist/portal.d.ts CHANGED
@@ -33,6 +33,14 @@ export interface PortalUser {
33
33
  createdAt: string;
34
34
  }
35
35
  /** Users, memberships, and billing records, beside the log's tables. */
36
+ /** What a registration says about the person and the organisation; every field optional in the store, the route decides what it requires. */
37
+ export interface Profile {
38
+ name?: string | null;
39
+ company?: string | null;
40
+ role?: string | null;
41
+ phone?: string | null;
42
+ telegram?: string | null;
43
+ }
36
44
  export declare class PortalStore {
37
45
  private readonly client;
38
46
  private readonly p;
@@ -41,7 +49,7 @@ export declare class PortalStore {
41
49
  private init;
42
50
  static hashPassword(password: string): string;
43
51
  static checkPassword(password: string, stored: string): boolean;
44
- createUser(email: string, password: string): Promise<PortalUser>;
52
+ createUser(email: string, password: string, profile?: Profile): Promise<PortalUser>;
45
53
  authenticate(email: string, password: string): Promise<PortalUser | null>;
46
54
  user(id: string): Promise<PortalUser | null>;
47
55
  addMember(userId: string, tenantId: string): Promise<void>;
@@ -58,12 +66,12 @@ export declare class PortalStore {
58
66
  status: string;
59
67
  } | null>;
60
68
  /** Every portal registration with its tenant and billing state, oldest first: what the operator's admin page lists. */
61
- registrations(): Promise<{
69
+ registrations(): Promise<({
62
70
  tenantId: string;
63
71
  email: string;
64
72
  registeredAt: string;
65
73
  billing: string | null;
66
- }[]>;
74
+ } & Profile)[]>;
67
75
  tenantBySubscription(subscriptionId: string): Promise<string | null>;
68
76
  }
69
77
  export declare function signSession(secret: string, userId: string, ttlMs?: number): string;
package/dist/portal.js CHANGED
@@ -19,7 +19,7 @@ const ident = (s) => {
19
19
  throw new Error(`prefix must be a plain identifier; got "${s}"`);
20
20
  return s;
21
21
  };
22
- /** Users, memberships, and billing records, beside the log's tables. */
22
+ const PROFILE_COLUMNS = ["name", "company", "role", "phone", "telegram"];
23
23
  export class PortalStore {
24
24
  client;
25
25
  p;
@@ -36,6 +36,9 @@ export class PortalStore {
36
36
  await this.client.query(`CREATE TABLE IF NOT EXISTS ${p}users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`);
37
37
  await this.client.query(`CREATE TABLE IF NOT EXISTS ${p}members (user_id TEXT NOT NULL REFERENCES ${p}users(id), tenant_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'owner', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (user_id, tenant_id))`);
38
38
  await this.client.query(`CREATE TABLE IF NOT EXISTS ${p}billing (tenant_id TEXT PRIMARY KEY, customer_id TEXT, subscription_id TEXT, status TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now())`);
39
+ // who the account belongs to: added after the first tenants registered, so the columns are optional
40
+ for (const c of PROFILE_COLUMNS)
41
+ await this.client.query(`ALTER TABLE ${p}users ADD COLUMN IF NOT EXISTS ${c} TEXT`);
39
42
  })();
40
43
  }
41
44
  return this.ready;
@@ -52,10 +55,10 @@ export class PortalStore {
52
55
  const got = scryptSync(password, Buffer.from(saltHex, "hex"), expected.length);
53
56
  return got.length === expected.length && timingSafeEqual(got, expected);
54
57
  }
55
- async createUser(email, password) {
58
+ async createUser(email, password, profile = {}) {
56
59
  await this.init();
57
60
  const id = randomUUID();
58
- const rows = (await this.client.query(`INSERT INTO ${this.p}users (id, email, password_hash) VALUES ($1, $2, $3) ON CONFLICT (email) DO NOTHING RETURNING id, email, created_at`, [id, email, PortalStore.hashPassword(password)])).rows;
61
+ const rows = (await this.client.query(`INSERT INTO ${this.p}users (id, email, password_hash, name, company, role, phone, telegram) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (email) DO NOTHING RETURNING id, email, created_at`, [id, email, PortalStore.hashPassword(password), profile.name ?? null, profile.company ?? null, profile.role ?? null, profile.phone ?? null, profile.telegram ?? null])).rows;
59
62
  if (!rows[0])
60
63
  throw new Error("an account with this email already exists");
61
64
  return { id, email, createdAt: new Date(rows[0].created_at).toISOString() };
@@ -98,8 +101,9 @@ export class PortalStore {
98
101
  /** Every portal registration with its tenant and billing state, oldest first: what the operator's admin page lists. */
99
102
  async registrations() {
100
103
  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 }));
104
+ const rows = (await this.client.query(`SELECT m.tenant_id, u.email, u.created_at, u.name, u.company, u.role, u.phone, u.telegram, 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;
105
+ const str = (v) => (v == null ? null : String(v));
106
+ return rows.map((r) => ({ tenantId: String(r.tenant_id), email: String(r.email), registeredAt: new Date(r.created_at).toISOString(), billing: str(r.status), name: str(r.name), company: str(r.company), role: str(r.role), phone: str(r.phone), telegram: str(r.telegram) }));
103
107
  }
104
108
  async tenantBySubscription(subscriptionId) {
105
109
  await this.init();
@@ -200,7 +204,7 @@ export function portalHandler(o) {
200
204
  return json(200, { ok: true, stripe: !!o.stripe });
201
205
  if (req.method === "GET" && url.pathname === "/") {
202
206
  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'" });
203
- return void res.end(PORTAL_PAGE);
207
+ return void res.end(PORTAL_PAGE.replace("__LOG_BASE__", base.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c])));
204
208
  }
205
209
  // ---- Stripe's webhook: the only caller that is not a browser with a session ----
206
210
  if (req.method === "POST" && url.pathname === "/stripe/webhook") {
@@ -234,8 +238,6 @@ export function portalHandler(o) {
234
238
  }
235
239
  // ---- registration and login ----
236
240
  if (req.method === "POST" && url.pathname === "/api/register") {
237
- if (!registrations.take(`reg:${addr}`))
238
- return json(429, { error: "too many registrations from this address; try again later" });
239
241
  const b = await jsonBody();
240
242
  const email = String(b.email ?? "").trim().toLowerCase();
241
243
  const password = String(b.password ?? "");
@@ -246,11 +248,24 @@ export function portalHandler(o) {
246
248
  return json(400, { error: "the password needs at least ten characters" });
247
249
  if (!TENANT_ID.test(tenant) || RESERVED.has(tenant))
248
250
  return json(400, { error: "the tenant id is the name in your log's URL: three to forty lowercase letters, digits, or hyphens, and not a reserved word" });
251
+ const text = (k, max) => String(b[k] ?? "").trim().slice(0, max);
252
+ const profile = { name: text("name", 120), company: text("company", 160), role: text("role", 120) || null, phone: text("phone", 40) || null, telegram: text("telegram", 40).replace(/^@/, "") || null };
253
+ if (!profile.name)
254
+ return json(400, { error: "your name is needed, so we know who to write to" });
255
+ if (!profile.company)
256
+ return json(400, { error: "the company or organisation the tenant is for is needed" });
257
+ if (profile.telegram && !/^[A-Za-z0-9_]{5,32}$/.test(profile.telegram))
258
+ return json(400, { error: "a Telegram username is five to thirty-two letters, digits, or underscores, with or without the @" });
259
+ if (profile.phone && !/^[+0-9 ()./-]{6,40}$/.test(profile.phone))
260
+ return json(400, { error: "a phone number is digits, with an optional + and spaces" });
249
261
  if (await o.tenancy.tenant(tenant))
250
262
  return json(409, { error: "that tenant id is taken" });
263
+ // throttled once the request is well formed: a mistyped form costs nothing, five real registrations from one address, then one every ten minutes
264
+ if (!registrations.take(`reg:${addr}`))
265
+ return json(429, { error: "too many registrations from this address; try again later" });
251
266
  let user;
252
267
  try {
253
- user = await store.createUser(email, password);
268
+ user = await store.createUser(email, password, profile);
254
269
  }
255
270
  catch (e) {
256
271
  return json(409, { error: e instanceof Error ? e.message : String(e) });
@@ -399,6 +414,7 @@ const PORTAL_PAGE = `<!doctype html>
399
414
  .bar i { display: block; width: 70%; background: var(--accent); border-radius: 3px 3px 0 0; min-height: 2px; }
400
415
  .bar b { font-family: var(--mono); font-weight: 400; margin-top: .3rem; }
401
416
  label { display: grid; gap: .25rem; font-size: .85rem; color: var(--ink2); margin: 0 0 .8rem; }
417
+ label .opt { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; margin-left: .3rem; } label .hint { font-size: .8rem; line-height: 1.45; } label .hint code { font-family: var(--mono); font-size: .9em; }
402
418
  input, select { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 4px; background: var(--bg); color: var(--ink); }
403
419
  button { font: inherit; padding: .5rem .9rem; border-radius: 4px; border: 1px solid var(--accent); background: var(--accent); color: #fff; cursor: pointer; }
404
420
  button.quiet { background: transparent; color: var(--accent); }
@@ -415,9 +431,18 @@ const PORTAL_PAGE = `<!doctype html>
415
431
  <section id="auth" class="auth" hidden>
416
432
  <h1 id="authTitle">Sign in</h1>
417
433
  <form id="authForm">
418
- <label>Email<input id="email" type="email" autocomplete="email" required></label>
434
+ <div id="regFields" hidden>
435
+ <label>Your name<input id="name" autocomplete="name" maxlength="120"></label>
436
+ <label>Company or organisation<input id="company" autocomplete="organization" maxlength="160"></label>
437
+ <label>Your role <span class="opt">optional</span><input id="role" autocomplete="organization-title" maxlength="120" placeholder="Head of Platform"></label>
438
+ </div>
439
+ <label>Work email<input id="email" type="email" autocomplete="email" required></label>
440
+ <div id="regFields2" hidden>
441
+ <label>Phone <span class="opt">optional</span><input id="phone" type="tel" autocomplete="tel" maxlength="40" placeholder="+44 20 …"></label>
442
+ <label>Telegram username <span class="opt">optional</span><input id="telegram" maxlength="40" placeholder="@yourname"><span class="hint">Your handle in Telegram, under Settings, if you would rather we reach you there than by email.</span></label>
443
+ </div>
419
444
  <label>Password<input id="password" type="password" autocomplete="current-password" minlength="10" required></label>
420
- <label id="tenantField" hidden>Tenant id, the name in your log's URL<input id="tenant" placeholder="acme" pattern="[a-z0-9][a-z0-9-]{1,38}[a-z0-9]"></label>
445
+ <label id="tenantField" hidden>Tenant id<input id="tenant" placeholder="acme" pattern="[a-z0-9][a-z0-9-]{1,38}[a-z0-9]"><span class="hint">A short name for your organisation, filled in from the company name; change it if you like. It becomes the path of your log, which your gateway config and your auditors will use: <code id="tenantPreview">__LOG_BASE__t/&lt;tenant&gt;/</code></span></label>
421
446
  <button id="authGo" type="submit">Sign in</button>
422
447
  <p class="msg" id="authMsg"></p>
423
448
  </form>
@@ -452,6 +477,7 @@ const PORTAL_PAGE = `<!doctype html>
452
477
  <div class="card" id="cKeys"><div class="k">Live keys</div><div class="v" id="vKeys">–</div><div class="s" id="sKeys"></div></div>
453
478
  <div class="card" id="cCp"><div class="k">Latest checkpoint</div><div class="v" id="vCp">–</div><div class="s" id="sCp"></div></div>
454
479
  </div>
480
+ <p class="muted">Tree heads on this log are signed by our key; there is no second, independent witness yet. Every receipt you log here is checkable by anyone with your gateway's public key and ours: <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>
455
481
  <h2>Your log</h2>
456
482
  <div class="panel"><table><tbody id="urls"></tbody></table></div>
457
483
  <h2>Recent activity</h2>
@@ -503,12 +529,18 @@ const PORTAL_PAGE = `<!doctype html>
503
529
  for (const a of document.querySelectorAll("nav a[data-view]")) a.classList.toggle("on", a.dataset.view === name);
504
530
  location.hash = name;
505
531
  };
506
- $("authSwap").onclick = () => { registering = !registering; $("authTitle").textContent = registering ? "Create your tenant" : "Sign in"; $("authGo").textContent = registering ? "Create tenant" : "Sign in"; $("tenantField").hidden = !registering; $("tenant").required = registering; $("password").autocomplete = registering ? "new-password" : "current-password"; $("authSwap").textContent = registering ? "I already have an account" : "Create an account and a tenant instead"; };
532
+ const LOG_BASE = "__LOG_BASE__";
533
+ const slug = (s) => s.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
534
+ let tenantEdited = false;
535
+ const previewTenant = () => { $("tenantPreview").textContent = LOG_BASE + "t/" + ($("tenant").value || "<tenant>") + "/"; };
536
+ $("company").oninput = () => { if (!tenantEdited) { $("tenant").value = slug($("company").value); previewTenant(); } };
537
+ $("tenant").oninput = () => { tenantEdited = $("tenant").value !== ""; previewTenant(); };
538
+ $("authSwap").onclick = () => { registering = !registering; $("authTitle").textContent = registering ? "Create your tenant" : "Sign in"; $("authGo").textContent = registering ? "Create tenant" : "Sign in"; for (const id of ["regFields", "regFields2", "tenantField"]) $(id).hidden = !registering; for (const id of ["name", "company", "tenant"]) $(id).required = registering; $("password").autocomplete = registering ? "new-password" : "current-password"; $("authSwap").textContent = registering ? "I already have an account" : "Create an account and a tenant instead"; previewTenant(); };
507
539
  $("authForm").onsubmit = async (e) => {
508
540
  e.preventDefault(); $("authMsg").className = "msg"; $("authMsg").textContent = "";
509
541
  try {
510
542
  if (registering) {
511
- const r = await api("POST", "/api/register", { email: $("email").value, password: $("password").value, tenant: $("tenant").value });
543
+ 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 });
512
544
  $("firstToken").textContent = r.token; $("firstSheet").textContent = r.welcome; show("welcome");
513
545
  } else { await api("POST", "/api/login", { email: $("email").value, password: $("password").value }); await enter(); }
514
546
  } catch (err) { $("authMsg").className = "msg err"; $("authMsg").textContent = err.message; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-custody/receipts",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
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": {