@agent-custody/receipts 0.4.0 → 0.5.1
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/README.md +6 -1
- package/dist/checkpoints.d.ts +26 -0
- package/dist/checkpoints.js +95 -0
- package/dist/cli.js +164 -15
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/log-admin.d.ts +33 -0
- package/dist/log-admin.js +235 -0
- package/dist/log-sink.d.ts +59 -3
- package/dist/log-sink.js +248 -71
- package/dist/log-store.d.ts +122 -0
- package/dist/log-store.js +312 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +1 -1
- package/dist/signer.d.ts +64 -0
- package/dist/signer.js +136 -0
- package/docs/usage.md +1 -1
- package/docs/verification.md +3 -1
- package/package.json +8 -3
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// The operator's admin surface for a hosted log: tenants and their tokens, over HTTP behind an admin token, and a
|
|
2
|
+
// single page at /admin that drives it. It is for whoever runs the log, never for tenants: every route needs the
|
|
3
|
+
// admin token, the page keeps that token in the browser session only, and a minted token is shown once, beside the
|
|
4
|
+
// welcome sheet the tenant gets. Nothing here touches receipts; the log holds hashes and the panel holds names.
|
|
5
|
+
import { timingSafeEqual } from "node:crypto";
|
|
6
|
+
const same = (a, b) => {
|
|
7
|
+
const x = Buffer.from(a);
|
|
8
|
+
const y = Buffer.from(b);
|
|
9
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
10
|
+
};
|
|
11
|
+
/** The welcome sheet as text, the same one deploy/onboard-tenant.sh prints. */
|
|
12
|
+
export function welcomeSheet(o) {
|
|
13
|
+
const base = o.publicUrl.endsWith("/") ? o.publicUrl : `${o.publicUrl}/`;
|
|
14
|
+
const url = `${base}t/${o.tenant}/`;
|
|
15
|
+
const lines = [
|
|
16
|
+
`agent-custody log: welcome sheet for tenant "${o.tenant}"`,
|
|
17
|
+
"",
|
|
18
|
+
`Your log ${url}`,
|
|
19
|
+
`Your log id ${o.logId}`,
|
|
20
|
+
...(o.checkpointsUrl ? [`Your checkpoints ${o.checkpointsUrl.replace(/\/?$/, "/")}${o.tenant}/latest.json`] : []),
|
|
21
|
+
`The log's keys ${base}.well-known/agent-custody-log.json${o.keyid ? ` (current keyid ${o.keyid})` : ""}`,
|
|
22
|
+
"",
|
|
23
|
+
"Your token was shown once when it was made; the log keeps only its hash. Lose it and ask for a new one.",
|
|
24
|
+
"",
|
|
25
|
+
"In your gateway or SDK config:",
|
|
26
|
+
` "log": { "url": "${url}", "tokenEnv": "AGENT_CUSTODY_LOG_TOKEN", "hashOnly": true }`,
|
|
27
|
+
"hashOnly means this log never receives your receipts, only their hashes.",
|
|
28
|
+
"",
|
|
29
|
+
"For whoever verifies your receipts:",
|
|
30
|
+
` npx agent-custody verify receipts/<id>.json --issuer-key <your gateway.pub> --principal-key <your principal.pub> --log-url ${url} --log-id ${o.logId}`,
|
|
31
|
+
` npx agent-custody audit --older receipts/<earlier>.json --newer receipts/<later>.json --log-url ${url} --log-id ${o.logId}`,
|
|
32
|
+
"--log-url fetches this log's published keys and pins them; --log-id makes sure the tree heads are this log's.",
|
|
33
|
+
"",
|
|
34
|
+
"What this log does not do: hold receipt contents, forge a receipt (your gateway key signs those), or, today,",
|
|
35
|
+
"countersign with a second independent witness. The proof table: https://agent-custody.dev/receipts/#what-a-receipt-proves-and-what-it-does-not",
|
|
36
|
+
];
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Routes under /admin. Returns true when it handled the request.
|
|
41
|
+
* GET /admin the page
|
|
42
|
+
* GET /admin/info { publicUrl, checkpointsUrl, keyid }
|
|
43
|
+
* GET /admin/tenants [{ id, logId, createdAt, disabledAt, tokens }]
|
|
44
|
+
* POST /admin/tenants { id, logId? } the tenant
|
|
45
|
+
* POST /admin/tenants/:id/disable
|
|
46
|
+
* GET /admin/tenants/:id/tokens [{ label, tokenHash, createdAt, revokedAt }]
|
|
47
|
+
* POST /admin/tenants/:id/tokens { label } { token, tokenHash, welcome } token shown once
|
|
48
|
+
* POST /admin/tenants/:id/tokens/:prefix/revoke { revoked }
|
|
49
|
+
*/
|
|
50
|
+
export function adminRoutes(opts) {
|
|
51
|
+
return async (req, res, url) => {
|
|
52
|
+
if (url.pathname !== "/admin" && !url.pathname.startsWith("/admin/"))
|
|
53
|
+
return false;
|
|
54
|
+
const json = (status, body) => {
|
|
55
|
+
res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
56
|
+
res.end(JSON.stringify(body));
|
|
57
|
+
};
|
|
58
|
+
if (req.method === "GET" && url.pathname === "/admin") {
|
|
59
|
+
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'" });
|
|
60
|
+
res.end(ADMIN_PAGE);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const h = req.headers.authorization ?? "";
|
|
64
|
+
if (!(h.startsWith("Bearer ") && h.length > 7 && same(h.slice(7), opts.token))) {
|
|
65
|
+
json(401, { error: "admin token required" });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const body = async () => {
|
|
69
|
+
let text = "";
|
|
70
|
+
for await (const chunk of req) {
|
|
71
|
+
text += chunk;
|
|
72
|
+
if (text.length > 16_384)
|
|
73
|
+
throw new Error("body too large");
|
|
74
|
+
}
|
|
75
|
+
return text ? JSON.parse(text) : {};
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
const t = opts.tenancy;
|
|
79
|
+
const parts = url.pathname.split("/").filter(Boolean); // ["admin", ...]
|
|
80
|
+
if (req.method === "GET" && parts.length === 2 && parts[1] === "info") {
|
|
81
|
+
json(200, { publicUrl: opts.publicUrl ?? null, checkpointsUrl: opts.checkpointsUrl ?? null, keyid: opts.keyid ?? null });
|
|
82
|
+
}
|
|
83
|
+
else if (req.method === "GET" && parts.length === 2 && parts[1] === "tenants") {
|
|
84
|
+
const tenants = await t.listTenants();
|
|
85
|
+
json(200, await Promise.all(tenants.map(async (x) => ({ ...x, tokens: (await t.listTokens(x.id)).filter((k) => !k.revokedAt).length }))));
|
|
86
|
+
}
|
|
87
|
+
else if (req.method === "POST" && parts.length === 2 && parts[1] === "tenants") {
|
|
88
|
+
const b = await body();
|
|
89
|
+
if (typeof b.id !== "string" || !/^[A-Za-z0-9_.-]+$/.test(b.id))
|
|
90
|
+
return json(400, { error: "id must be a plain identifier" }), true;
|
|
91
|
+
json(200, await t.addTenant(b.id, typeof b.logId === "string" && b.logId ? b.logId : b.id));
|
|
92
|
+
}
|
|
93
|
+
else if (req.method === "POST" && parts.length === 4 && parts[1] === "tenants" && parts[3] === "disable") {
|
|
94
|
+
await t.disableTenant(parts[2]);
|
|
95
|
+
json(200, { disabled: parts[2] });
|
|
96
|
+
}
|
|
97
|
+
else if (req.method === "GET" && parts.length === 4 && parts[1] === "tenants" && parts[3] === "tokens") {
|
|
98
|
+
json(200, await t.listTokens(parts[2]));
|
|
99
|
+
}
|
|
100
|
+
else if (req.method === "POST" && parts.length === 4 && parts[1] === "tenants" && parts[3] === "tokens") {
|
|
101
|
+
const b = await body();
|
|
102
|
+
const label = typeof b.label === "string" && b.label.trim() ? b.label.trim() : "fleet";
|
|
103
|
+
const tenant = await t.tenant(parts[2]);
|
|
104
|
+
if (!tenant)
|
|
105
|
+
return json(404, { error: "unknown tenant" }), true;
|
|
106
|
+
const minted = await t.addToken(tenant.id, label);
|
|
107
|
+
const welcome = opts.publicUrl ? welcomeSheet({ tenant: tenant.id, logId: tenant.logId, publicUrl: opts.publicUrl, ...(opts.checkpointsUrl ? { checkpointsUrl: opts.checkpointsUrl } : {}), ...(opts.keyid ? { keyid: opts.keyid } : {}) }) : null;
|
|
108
|
+
json(200, { ...minted, welcome });
|
|
109
|
+
}
|
|
110
|
+
else if (req.method === "POST" && parts.length === 6 && parts[1] === "tenants" && parts[3] === "tokens" && parts[5] === "revoke") {
|
|
111
|
+
json(200, { revoked: await t.revokeToken(parts[2], parts[4]) });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
json(404, { error: "not found" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
json(400, { error: e instanceof Error ? e.message : String(e) });
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** The page. One file, no framework, no third-party requests; the admin token lives in sessionStorage for the tab. */
|
|
124
|
+
const ADMIN_PAGE = `<!doctype html>
|
|
125
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
126
|
+
<title>agent-custody log admin</title>
|
|
127
|
+
<style>
|
|
128
|
+
: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; }
|
|
129
|
+
@media (prefers-color-scheme: dark) { :root { --ink: #e6ecf0; --ink2: #9fb0bd; --line: #27333c; --bg: #0e1418; --panel: #151d23; --accent: #4fc3b0; --warn: #e2b862; --warnbg: #2d2412; } }
|
|
130
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
|
|
131
|
+
main { max-width: 72rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
132
|
+
h1 { font-size: 1.4rem; margin: 0 0 .25rem; } h2 { font-size: 1.05rem; margin: 2rem 0 .75rem; }
|
|
133
|
+
.sub { color: var(--ink2); margin: 0 0 1.5rem; }
|
|
134
|
+
.row { display: flex; gap: .6rem; flex-wrap: wrap; align-items: end; }
|
|
135
|
+
label { display: grid; gap: .25rem; font-size: .85rem; color: var(--ink2); }
|
|
136
|
+
input { font: inherit; padding: .45rem .6rem; border: 1px solid var(--line); border-radius: 4px; background: var(--panel); color: var(--ink); min-width: 14rem; }
|
|
137
|
+
button { font: inherit; padding: .5rem .9rem; border: 1px solid var(--accent); border-radius: 4px; background: var(--accent); color: #fff; cursor: pointer; }
|
|
138
|
+
button.quiet { background: transparent; color: var(--accent); }
|
|
139
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
140
|
+
table { border-collapse: collapse; width: 100%; font-size: .93rem; }
|
|
141
|
+
th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
142
|
+
th { font-size: .78rem; letter-spacing: .04em; text-transform: uppercase; color: var(--ink2); }
|
|
143
|
+
code, pre { font-family: var(--mono); font-size: .86em; }
|
|
144
|
+
pre { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; padding: .9rem 1rem; overflow-x: auto; white-space: pre-wrap; }
|
|
145
|
+
.once { border-left: 3px solid var(--warn); background: var(--warnbg); padding: .8rem 1rem; border-radius: 0 4px 4px 0; margin: 1rem 0; }
|
|
146
|
+
.muted { color: var(--ink2); } .err { color: #b3261e; } .ok { color: var(--accent); }
|
|
147
|
+
.tok { font-family: var(--mono); font-size: 1.05rem; word-break: break-all; user-select: all; }
|
|
148
|
+
[hidden] { display: none !important; }
|
|
149
|
+
</style>
|
|
150
|
+
<main>
|
|
151
|
+
<h1>Log admin</h1>
|
|
152
|
+
<p class="sub" id="where">Tenants and tokens on this log. The admin token stays in this tab.</p>
|
|
153
|
+
<section id="login">
|
|
154
|
+
<div class="row"><label>Admin token<input id="token" type="password" autocomplete="off"></label><button id="enter">Enter</button></div>
|
|
155
|
+
<p class="err" id="loginErr" hidden></p>
|
|
156
|
+
</section>
|
|
157
|
+
<section id="app" hidden>
|
|
158
|
+
<h2>Tenants</h2>
|
|
159
|
+
<table><thead><tr><th>tenant</th><th>log id</th><th>live tokens</th><th>created</th><th></th></tr></thead><tbody id="tenants"></tbody></table>
|
|
160
|
+
<h2>New tenant</h2>
|
|
161
|
+
<div class="row">
|
|
162
|
+
<label>tenant id (in the URL)<input id="tid" placeholder="acme" autocomplete="off"></label>
|
|
163
|
+
<label>log id (on tree heads; default = tenant id)<input id="lid" placeholder="acme-eu" autocomplete="off"></label>
|
|
164
|
+
<button id="addTenant">Create</button>
|
|
165
|
+
</div>
|
|
166
|
+
<h2>New token</h2>
|
|
167
|
+
<div class="row">
|
|
168
|
+
<label>tenant<input id="ttid" placeholder="acme" autocomplete="off"></label>
|
|
169
|
+
<label>label (which fleet)<input id="label" placeholder="support fleet" autocomplete="off"></label>
|
|
170
|
+
<button id="mint">Mint token</button>
|
|
171
|
+
</div>
|
|
172
|
+
<div id="minted" class="once" hidden>
|
|
173
|
+
<p><b>Shown once.</b> Hand it over by a channel you trust; the log keeps only its hash.</p>
|
|
174
|
+
<p class="tok" id="tokval"></p>
|
|
175
|
+
<button class="quiet" id="copyTok">Copy token</button> <button class="quiet" id="copySheet">Copy welcome sheet</button>
|
|
176
|
+
<pre id="sheet"></pre>
|
|
177
|
+
</div>
|
|
178
|
+
<h2>Tokens of a tenant</h2>
|
|
179
|
+
<div class="row"><label>tenant<input id="ltid" placeholder="acme" autocomplete="off"></label><button class="quiet" id="listTokens">List</button></div>
|
|
180
|
+
<table><thead><tr><th>label</th><th>hash</th><th>created</th><th>state</th><th></th></tr></thead><tbody id="tokens"></tbody></table>
|
|
181
|
+
<p class="muted" id="msg"></p>
|
|
182
|
+
</section>
|
|
183
|
+
</main>
|
|
184
|
+
<script>
|
|
185
|
+
(() => {
|
|
186
|
+
const $ = (id) => document.getElementById(id);
|
|
187
|
+
let token = sessionStorage.getItem("agent-custody-admin") || "";
|
|
188
|
+
const api = async (method, path, body) => {
|
|
189
|
+
const r = await fetch(path, { method, headers: { authorization: "Bearer " + token, ...(body ? { "content-type": "application/json" } : {}) }, body: body ? JSON.stringify(body) : undefined });
|
|
190
|
+
const j = await r.json().catch(() => ({}));
|
|
191
|
+
if (!r.ok) throw new Error(j.error || r.statusText);
|
|
192
|
+
return j;
|
|
193
|
+
};
|
|
194
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
195
|
+
const say = (t, cls) => { $("msg").textContent = t; $("msg").className = cls || "muted"; };
|
|
196
|
+
const loadTenants = async () => {
|
|
197
|
+
const list = await api("GET", "/admin/tenants");
|
|
198
|
+
$("tenants").innerHTML = list.map((t) => "<tr><td><code>" + esc(t.id) + "</code></td><td><code>" + esc(t.logId) + "</code></td><td>" + t.tokens + "</td><td>" + esc(t.createdAt.slice(0, 10)) + "</td><td>" + (t.disabledAt ? "<span class=muted>disabled</span>" : "<button class=quiet data-disable=\\"" + esc(t.id) + "\\">Disable</button>") + "</td></tr>").join("") || "<tr><td colspan=5 class=muted>none yet</td></tr>";
|
|
199
|
+
};
|
|
200
|
+
const loadTokens = async (id) => {
|
|
201
|
+
const list = await api("GET", "/admin/tenants/" + encodeURIComponent(id) + "/tokens");
|
|
202
|
+
$("tokens").innerHTML = list.map((k) => "<tr><td>" + esc(k.label) + "</td><td><code>" + esc(k.tokenHash.slice(0, 12)) + "</code></td><td>" + esc(k.createdAt.slice(0, 10)) + "</td><td>" + (k.revokedAt ? "revoked " + esc(k.revokedAt.slice(0, 10)) : "<span class=ok>live</span>") + "</td><td>" + (k.revokedAt ? "" : "<button class=quiet data-revoke=\\"" + esc(id) + "|" + esc(k.tokenHash.slice(0, 12)) + "\\">Revoke</button>") + "</td></tr>").join("") || "<tr><td colspan=5 class=muted>no tokens</td></tr>";
|
|
203
|
+
};
|
|
204
|
+
const enter = async () => {
|
|
205
|
+
try {
|
|
206
|
+
const info = await api("GET", "/admin/info");
|
|
207
|
+
$("where").textContent = (info.publicUrl || location.origin) + " · keyid " + (info.keyid ? info.keyid.slice(0, 12) : "?") + (info.checkpointsUrl ? " · checkpoints at " + info.checkpointsUrl : "");
|
|
208
|
+
$("login").hidden = true; $("app").hidden = false;
|
|
209
|
+
sessionStorage.setItem("agent-custody-admin", token);
|
|
210
|
+
await loadTenants();
|
|
211
|
+
} catch (e) { $("loginErr").hidden = false; $("loginErr").textContent = e.message; }
|
|
212
|
+
};
|
|
213
|
+
$("enter").onclick = () => { token = $("token").value.trim(); enter(); };
|
|
214
|
+
$("token").onkeydown = (e) => { if (e.key === "Enter") $("enter").click(); };
|
|
215
|
+
$("addTenant").onclick = async () => { try { const t = await api("POST", "/admin/tenants", { id: $("tid").value.trim(), logId: $("lid").value.trim() }); say("tenant " + t.id + " created; reached at /t/" + t.id + "/", "ok"); $("ttid").value = t.id; await loadTenants(); } catch (e) { say(e.message, "err"); } };
|
|
216
|
+
$("mint").onclick = async () => {
|
|
217
|
+
try {
|
|
218
|
+
const r = await api("POST", "/admin/tenants/" + encodeURIComponent($("ttid").value.trim()) + "/tokens", { label: $("label").value.trim() });
|
|
219
|
+
$("tokval").textContent = r.token; $("sheet").textContent = r.welcome || "(set --public-url on the server for the welcome sheet)"; $("minted").hidden = false;
|
|
220
|
+
say("token minted for " + $("ttid").value.trim() + "; stored as hash " + r.tokenHash.slice(0, 12), "ok");
|
|
221
|
+
await loadTenants();
|
|
222
|
+
} catch (e) { say(e.message, "err"); }
|
|
223
|
+
};
|
|
224
|
+
$("copyTok").onclick = () => navigator.clipboard.writeText($("tokval").textContent).then(() => say("token copied", "ok"));
|
|
225
|
+
$("copySheet").onclick = () => navigator.clipboard.writeText($("sheet").textContent).then(() => say("welcome sheet copied", "ok"));
|
|
226
|
+
$("listTokens").onclick = () => loadTokens($("ltid").value.trim()).catch((e) => say(e.message, "err"));
|
|
227
|
+
document.addEventListener("click", async (e) => {
|
|
228
|
+
const b = e.target.closest("button"); if (!b) return;
|
|
229
|
+
if (b.dataset.disable && confirm("Disable tenant " + b.dataset.disable + "? Its paths answer 404 within ten seconds.")) { try { await api("POST", "/admin/tenants/" + encodeURIComponent(b.dataset.disable) + "/disable"); await loadTenants(); say("disabled " + b.dataset.disable, "ok"); } catch (err) { say(err.message, "err"); } }
|
|
230
|
+
if (b.dataset.revoke) { const [id, prefix] = b.dataset.revoke.split("|"); if (confirm("Revoke token " + prefix + " of " + id + "?")) { try { await api("POST", "/admin/tenants/" + encodeURIComponent(id) + "/tokens/" + prefix + "/revoke"); await loadTokens(id); await loadTenants(); say("revoked", "ok"); } catch (err) { say(err.message, "err"); } } }
|
|
231
|
+
});
|
|
232
|
+
if (token) enter();
|
|
233
|
+
})();
|
|
234
|
+
</script>
|
|
235
|
+
`;
|
package/dist/log-sink.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
2
|
import { type Envelope, type KeyPair } from "./crypto.ts";
|
|
3
3
|
import { type InclusionProof } from "./log.ts";
|
|
4
|
+
import { type LogBackend, type PostgresTenancy, type RateLimitOptions } from "./log-store.ts";
|
|
5
|
+
import { type Signer } from "./signer.ts";
|
|
6
|
+
import type { Checkpoint, CheckpointStore } from "./checkpoints.ts";
|
|
7
|
+
import { type AdminOptions } from "./log-admin.ts";
|
|
4
8
|
export interface LogAppend {
|
|
5
9
|
inclusion: InclusionProof;
|
|
6
10
|
/** signed TreeHead; the signature's keyid says who runs the log */
|
|
@@ -19,9 +23,14 @@ export interface HttpLogOptions {
|
|
|
19
23
|
token?: string;
|
|
20
24
|
/** send only the leaf hash; the log then commits to the receipt without ever holding it. Use it for any log run by someone else. */
|
|
21
25
|
hashOnly?: boolean;
|
|
26
|
+
/** attempts on 429 and 5xx; default 3 */
|
|
27
|
+
retries?: number;
|
|
22
28
|
fetch?: typeof fetch;
|
|
23
29
|
}
|
|
24
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* A log reached over HTTP: POST <url>/append with {leaf} or {leafHash}, expecting a LogAppend back. A 429 or a 5xx
|
|
32
|
+
* is retried a few times with backoff, honouring Retry-After; anything else, or the last failure, is the caller's.
|
|
33
|
+
*/
|
|
25
34
|
export declare function httpLog(url: string, opts?: HttpLogOptions): LogSink;
|
|
26
35
|
export interface LogConfig {
|
|
27
36
|
logFile?: string | undefined;
|
|
@@ -48,6 +57,53 @@ export interface LogServerOptions {
|
|
|
48
57
|
tokens?: string[];
|
|
49
58
|
logId?: string;
|
|
50
59
|
}>;
|
|
60
|
+
/** appends per token (or per address without one); default 50 a second, burst 100 */
|
|
61
|
+
rateLimit?: RateLimitOptions;
|
|
62
|
+
/** largest append body accepted, in bytes; default 65536 */
|
|
63
|
+
maxBodyBytes?: number;
|
|
64
|
+
/** where published checkpoints go and are listed from; without one, /checkpoints answers with none */
|
|
65
|
+
checkpoints?: CheckpointStore;
|
|
66
|
+
/** the operator's admin API and page under /admin, behind its own token; only with a Postgres tenancy */
|
|
67
|
+
admin?: AdminOptions;
|
|
68
|
+
}
|
|
69
|
+
/** One log as the handler sees it, whatever stands behind it. */
|
|
70
|
+
export interface ResolvedLog {
|
|
71
|
+
backend: LogBackend;
|
|
72
|
+
logId: string | undefined;
|
|
73
|
+
authorize(token: string | null): Promise<boolean>;
|
|
74
|
+
}
|
|
75
|
+
/** Turns the tenant in a path, or null for the root paths, into a log. */
|
|
76
|
+
export interface LogResolver {
|
|
77
|
+
resolve(tenant: string | null): Promise<ResolvedLog | null>;
|
|
78
|
+
/** every log this server has, null for the root one; what the checkpoint publisher walks */
|
|
79
|
+
tenants(): Promise<(string | null)[]>;
|
|
80
|
+
}
|
|
81
|
+
/** The reference server's logs: one file for the root paths and, optionally, a file per tenant from the options. */
|
|
82
|
+
export declare function fileResolver(file: string, opts?: LogServerOptions): LogResolver;
|
|
83
|
+
/**
|
|
84
|
+
* Logs in Postgres: every tenant from the tenants table, each with its own log and tokens; the root paths serve the
|
|
85
|
+
* tenant named `defaultTenant`, which also accepts `staticTokens` so a server can keep its environment token.
|
|
86
|
+
*/
|
|
87
|
+
export declare function postgresResolver(tenancy: PostgresTenancy, opts?: {
|
|
88
|
+
defaultTenant?: string;
|
|
89
|
+
staticTokens?: string[];
|
|
90
|
+
}): LogResolver;
|
|
91
|
+
/**
|
|
92
|
+
* Publishes one checkpoint per log whose tree has grown since the last one: the current head, signed, into the
|
|
93
|
+
* checkpoint store. Call publishOnce on a timer, or start() to run it every `everyMs`.
|
|
94
|
+
*/
|
|
95
|
+
export declare class CheckpointPublisher {
|
|
96
|
+
private timer;
|
|
97
|
+
private readonly resolver;
|
|
98
|
+
private readonly signer;
|
|
99
|
+
private readonly store;
|
|
100
|
+
private readonly everyMs;
|
|
101
|
+
private readonly warn;
|
|
102
|
+
constructor(resolver: LogResolver, signer: Signer, store: CheckpointStore, everyMs?: number, warn?: (m: string) => void);
|
|
103
|
+
/** Publishes for every log that has grown; returns the checkpoints written. */
|
|
104
|
+
publishOnce(): Promise<Checkpoint[]>;
|
|
105
|
+
start(): void;
|
|
106
|
+
stop(): void;
|
|
51
107
|
}
|
|
52
108
|
/**
|
|
53
109
|
* The reference log server as a node:http request handler.
|
|
@@ -56,13 +112,13 @@ export interface LogServerOptions {
|
|
|
56
112
|
* GET /consistency?old=M&new=N -> {oldSize, newSize, hashes}, proof that the log at N extends the log at M
|
|
57
113
|
* GET /head -> {treeHead}, the current tree head signed with the log's key
|
|
58
114
|
*/
|
|
59
|
-
export declare function logHandler(
|
|
115
|
+
export declare function logHandler(source: string | LogResolver, keyOrSigner: KeyPair | Signer, opts?: LogServerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
60
116
|
export interface RunningLog {
|
|
61
117
|
url: string;
|
|
62
118
|
close(): Promise<void>;
|
|
63
119
|
}
|
|
64
120
|
/** Starts the reference log server. Port 0 picks a free port. */
|
|
65
|
-
export declare function serveLog(
|
|
121
|
+
export declare function serveLog(source: string | LogResolver, keyOrSigner: KeyPair | Signer, opts: LogServerOptions & {
|
|
66
122
|
port: number;
|
|
67
123
|
host?: string;
|
|
68
124
|
}): Promise<RunningLog>;
|