sebbi 0.0.0-stage → 1.1.0
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/LICENSE +21 -0
- package/README.md +78 -2
- package/index.cjs +293 -0
- package/index.d.ts +25 -0
- package/index.mjs +3 -0
- package/package.json +16 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Monop Content
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sebbi: a receipt for every AI agent action
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Your agent refunds a customer, sends an email, moves money or answers a patient.
|
|
4
|
+
Add one line and every one of those actions gets a **sealed receipt with a public link anyone can check**:
|
|
5
|
+
your customer, your auditor, your insurer, a regulator.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install sebbi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { receipt, last } from "sebbi";
|
|
13
|
+
|
|
14
|
+
const decideRefund = receipt("refund-decision", async (order) => {
|
|
15
|
+
return { refund: true, amount: 42 };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await decideRefund({ id: 7 });
|
|
19
|
+
console.log((await last()).url); // https://sebbi.pro/r/... anyone can check it
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No account needed to start. Zero dependencies. Node 18+, Bun, Deno, edge runtimes and browsers. ESM and CommonJS. Typed.
|
|
23
|
+
|
|
24
|
+
## Why teams add it
|
|
25
|
+
|
|
26
|
+
- **Proof, not logs.** Each receipt sits in a Merkle tree sealed into the sebbi.pro chain, witnessed by independent chains, timestamped into Bitcoin and signed post-quantum. Nobody can quietly edit it afterwards.
|
|
27
|
+
- **Your data stays yours.** Inputs and outputs are fingerprinted (SHA-256) where your code runs. Only fingerprints, a short label and a timestamp are sent.
|
|
28
|
+
- **Never slows your agent.** Sealing runs in the background in batches and never throws into your code.
|
|
29
|
+
- **Reversals done properly.** Refund, cancel or withdraw an action and the original receipt is shown as reversed, linked to a sealed reversal receipt. Nothing disappears.
|
|
30
|
+
|
|
31
|
+
## Your framework, one line
|
|
32
|
+
|
|
33
|
+
**Vercel AI SDK**
|
|
34
|
+
```js
|
|
35
|
+
import { wrapTools } from "sebbi";
|
|
36
|
+
const result = await generateText({ model, prompt, tools: wrapTools({ getWeather, sendEmail }) });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**LangChain.js / Mastra / anything with tool objects**: `wrapTools([...tools])` wraps each tool's `execute`, `func` or `invoke`.
|
|
40
|
+
|
|
41
|
+
**MCP servers (TypeScript SDK)**
|
|
42
|
+
```js
|
|
43
|
+
server.tool("lookup_customer", schema, receipt("lookup_customer", async (args) => { ... }));
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Anything else**
|
|
47
|
+
```js
|
|
48
|
+
import { seal } from "sebbi";
|
|
49
|
+
const r = await seal(request, result, "send-invoice");
|
|
50
|
+
await r.wait(); // optional
|
|
51
|
+
console.log(r.url, r.sealed, r.blockIndex);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Reversing an action
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { reverse } from "sebbi";
|
|
58
|
+
await reverse(r, "Customer cancelled; refund issued");
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The original receipt stays sealed and valid, and its page shows **Reversed**, the reason and a link to the reversal receipt. Only whoever sealed the original (this installation, or the same account key) can reverse it, and a reversal cannot itself be reversed.
|
|
62
|
+
|
|
63
|
+
## Settings
|
|
64
|
+
|
|
65
|
+
| Environment | Meaning |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `SEBBI_API_KEY` | Your key from https://sebbi.pro/connect. Optional. |
|
|
68
|
+
| `SEBBI_DISABLED=1` | Turn every receipt into a no-op. |
|
|
69
|
+
| `SEBBI_OWNER_SECRET` | Fix the owner secret (serverless, several machines). |
|
|
70
|
+
|
|
71
|
+
Or in code: `configure({ apiKey: "..." })`.
|
|
72
|
+
|
|
73
|
+
## Pricing
|
|
74
|
+
|
|
75
|
+
- **Free lane:** 1,000 receipts a day, no account, no key.
|
|
76
|
+
- **With a sebbi.pro account:** up to 100,000 a day, tied to your account and your evidence packs. 7 days free, then 50p per device per month.
|
|
77
|
+
- **Checking any receipt is always free:** `check(id)` or https://sebbi.pro/r/<id>.
|
|
78
|
+
|
|
79
|
+
Docs: https://sebbi.pro/receipts · MCP server: https://sebbi.pro/mcp · Made by Monop Content.
|
package/index.cjs
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* sebbi - a receipt for every AI agent action. https://sebbi.pro/receipts
|
|
4
|
+
*
|
|
5
|
+
* npm install sebbi
|
|
6
|
+
*
|
|
7
|
+
* const { receipt, last } = require("sebbi"); // or: import { receipt } from "sebbi"
|
|
8
|
+
* const decide = receipt("refund-decision", async (order) => ({ refund: true }));
|
|
9
|
+
* await decide({ id: 7 });
|
|
10
|
+
* console.log((await last()).url); // https://sebbi.pro/r/...
|
|
11
|
+
*
|
|
12
|
+
* Inputs and outputs are fingerprinted (SHA-256 of canonical JSON) where your
|
|
13
|
+
* code runs. Only fingerprints, a short label and a timestamp are sent.
|
|
14
|
+
* Sealing runs in the background in batches and never throws into your code.
|
|
15
|
+
* Zero dependencies. Node 18+, Deno, Bun, edge runtimes and browsers.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const VERSION = "1.1.0";
|
|
19
|
+
const SITE = "https://sebbi.pro";
|
|
20
|
+
const env = (k) => (typeof process !== "undefined" && process.env ? process.env[k] || "" : "");
|
|
21
|
+
|
|
22
|
+
const cfg = {
|
|
23
|
+
apiKey: env("SEBBI_API_KEY"),
|
|
24
|
+
endpoint: env("SEBBI_ENDPOINT") || SITE + "/v1/receipts",
|
|
25
|
+
enabled: !["1", "true", "yes", "on"].includes(env("SEBBI_DISABLED").toLowerCase()),
|
|
26
|
+
client: "sebbi-js/" + VERSION,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function configure(o = {}) {
|
|
30
|
+
if (o.apiKey !== undefined) cfg.apiKey = o.apiKey;
|
|
31
|
+
if (o.endpoint !== undefined) cfg.endpoint = o.endpoint;
|
|
32
|
+
if (o.enabled !== undefined) cfg.enabled = !!o.enabled;
|
|
33
|
+
if (o.client !== undefined) cfg.client = o.client;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ------------------------------------------------------------ fingerprints
|
|
37
|
+
const subtle = () => (globalThis.crypto && globalThis.crypto.subtle) || require("node:crypto").webcrypto.subtle;
|
|
38
|
+
const hex = (buf) => Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
39
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
40
|
+
|
|
41
|
+
function canonical(v, depth = 0) {
|
|
42
|
+
if (depth > 32) return "__sebbi_deep__";
|
|
43
|
+
if (v === null || v === undefined) return null;
|
|
44
|
+
const t = typeof v;
|
|
45
|
+
if (t === "string" || t === "boolean") return v;
|
|
46
|
+
if (t === "number") return Number.isFinite(v) ? v : String(v);
|
|
47
|
+
if (t === "bigint") return v.toString();
|
|
48
|
+
if (t === "function" || t === "symbol") return { __type__: t };
|
|
49
|
+
if (v instanceof Date) return v.toISOString();
|
|
50
|
+
if (Array.isArray(v)) return v.map((x) => canonical(x, depth + 1));
|
|
51
|
+
if (v instanceof Map) return canonical(Object.fromEntries(v), depth + 1);
|
|
52
|
+
if (v instanceof Set) return canonical([...v], depth + 1);
|
|
53
|
+
if (typeof v.toJSON === "function") return canonical(v.toJSON(), depth + 1);
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const k of Object.keys(v)) out[k] = canonical(v[k], depth + 1);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stringify(v) {
|
|
60
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
61
|
+
if (Array.isArray(v)) return "[" + v.map(stringify).join(",") + "]";
|
|
62
|
+
return "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + stringify(v[k])).join(",") + "}";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ascii = (s) => s.replace(/[\u007f-]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
66
|
+
|
|
67
|
+
async function fingerprint(value) {
|
|
68
|
+
return hex(await subtle().digest("SHA-256", enc(ascii(stringify(canonical(value))))));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ------------------------------------------------------------- ownership
|
|
72
|
+
// Each receipt commits to a secret only this installation holds, so only you
|
|
73
|
+
// can reverse your own receipts. Per-receipt secrets are derived from one
|
|
74
|
+
// master, so revealing one to reverse a receipt reveals nothing else.
|
|
75
|
+
let masterP = null;
|
|
76
|
+
function master() {
|
|
77
|
+
if (masterP) return masterP;
|
|
78
|
+
masterP = (async () => {
|
|
79
|
+
const e = env("SEBBI_OWNER_SECRET");
|
|
80
|
+
if (e) return e;
|
|
81
|
+
const rnd = () => hex(globalThis.crypto ? globalThis.crypto.getRandomValues(new Uint8Array(32)) : require("node:crypto").randomBytes(32));
|
|
82
|
+
try {
|
|
83
|
+
const fs = require("node:fs"), path = require("node:path"), os = require("node:os");
|
|
84
|
+
const p = path.join(os.homedir(), ".sebbi", "owner_secret");
|
|
85
|
+
try { const v = fs.readFileSync(p, "utf8").trim(); if (v.length >= 32) return v; } catch (_) {}
|
|
86
|
+
const v = rnd();
|
|
87
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
88
|
+
fs.writeFileSync(p, v, { mode: 0o600, flag: "wx" });
|
|
89
|
+
return v;
|
|
90
|
+
} catch (_) {
|
|
91
|
+
return rnd(); // browsers, edge, read-only disks: this process can still reverse what it sealed
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
return masterP;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function secretFor(id) {
|
|
98
|
+
const key = await subtle().importKey("raw", enc(await master()), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
99
|
+
return hex(await subtle().sign("HMAC", key, enc("sebbi-owner|" + id)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sha = async (s) => hex(await subtle().digest("SHA-256", enc(s)));
|
|
103
|
+
const newId = () => "r" + hex(globalThis.crypto ? globalThis.crypto.getRandomValues(new Uint8Array(12)) : require("node:crypto").randomBytes(12)).slice(0, 23);
|
|
104
|
+
|
|
105
|
+
// ------------------------------------------------------------------ receipt
|
|
106
|
+
class Receipt {
|
|
107
|
+
constructor(label, inputHash, outputHash, outcome) {
|
|
108
|
+
this.id = newId();
|
|
109
|
+
this.label = String(label || "").slice(0, 80);
|
|
110
|
+
this.inputHash = inputHash;
|
|
111
|
+
this.outputHash = outputHash;
|
|
112
|
+
this.ts = Date.now() / 1000;
|
|
113
|
+
this.outcome = outcome || "ok";
|
|
114
|
+
this.sealed = false;
|
|
115
|
+
this.error = null;
|
|
116
|
+
this.blockIndex = null;
|
|
117
|
+
this.merkleRoot = null;
|
|
118
|
+
this.reverses = null;
|
|
119
|
+
this.reason = "";
|
|
120
|
+
this._done = new Promise((r) => (this._resolve = r));
|
|
121
|
+
}
|
|
122
|
+
get url() { return SITE + "/r/" + this.id; }
|
|
123
|
+
/** Resolves once sealed (or failed). */
|
|
124
|
+
wait(ms = 15000) { kick(); return Promise.race([this._done, new Promise((r) => setTimeout(() => r(this), ms))]); }
|
|
125
|
+
async wire() {
|
|
126
|
+
const ih = this.inputHash, oh = this.outputHash;
|
|
127
|
+
const d = { local_id: this.id, label: this.label, input_hash: ih, output_hash: oh, hash: await sha(ih + ":" + oh),
|
|
128
|
+
ts: this.ts, outcome: this.outcome, owner: await sha(await secretFor(this.id)) };
|
|
129
|
+
if (this.reverses) Object.assign(d, { reverses: this.reverses, reason: this.reason, owner_secret: await secretFor(this.reverses) });
|
|
130
|
+
return d;
|
|
131
|
+
}
|
|
132
|
+
toJSON() {
|
|
133
|
+
return { id: this.id, url: this.url, label: this.label, inputHash: this.inputHash, outputHash: this.outputHash, ts: this.ts,
|
|
134
|
+
outcome: this.outcome, sealed: this.sealed, error: this.error, blockIndex: this.blockIndex, merkleRoot: this.merkleRoot,
|
|
135
|
+
reverses: this.reverses };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ------------------------------------------------------------------ sender
|
|
140
|
+
const queue = [];
|
|
141
|
+
const counts = { queued: 0, sealed: 0, failed: 0 };
|
|
142
|
+
let notice = null, timer = null, inflight = 0;
|
|
143
|
+
|
|
144
|
+
function kick(delay = 0) {
|
|
145
|
+
if (timer) { if (delay) return; clearTimeout(timer); }
|
|
146
|
+
timer = setTimeout(drain, delay);
|
|
147
|
+
if (timer && typeof timer.unref === "function" && delay) timer.unref();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function drain() {
|
|
151
|
+
timer = null;
|
|
152
|
+
while (queue.length) {
|
|
153
|
+
const batch = queue.splice(0, 100);
|
|
154
|
+
inflight += batch.length;
|
|
155
|
+
try { await send(batch); } finally { inflight -= batch.length; }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function send(batch) {
|
|
160
|
+
let err = null;
|
|
161
|
+
const body = JSON.stringify({ records: await Promise.all(batch.map((r) => r.wire())), sdk: cfg.client });
|
|
162
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
163
|
+
if (cfg.apiKey) headers.Authorization = "Bearer " + cfg.apiKey;
|
|
164
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(cfg.endpoint, { method: "POST", headers, body });
|
|
167
|
+
const doc = await res.json().catch(() => ({}));
|
|
168
|
+
if (doc.message) notice = doc.message;
|
|
169
|
+
if (res.ok) return apply(batch, doc);
|
|
170
|
+
err = "http_" + res.status + (doc.error ? ":" + doc.error : "");
|
|
171
|
+
if (res.status < 500 && res.status !== 429) break;
|
|
172
|
+
} catch (e) {
|
|
173
|
+
err = (e && e.name) || "network";
|
|
174
|
+
}
|
|
175
|
+
await new Promise((r) => setTimeout(r, Math.min(8000, 500 * 2 ** attempt)));
|
|
176
|
+
}
|
|
177
|
+
for (const r of batch) { r.error = err; counts.failed++; r._resolve(r); }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function apply(batch, doc) {
|
|
181
|
+
const by = {};
|
|
182
|
+
for (const x of doc.records || []) if (x && x.local_id) by[x.local_id] = x;
|
|
183
|
+
for (const r of batch) {
|
|
184
|
+
const x = by[r.id];
|
|
185
|
+
if (x) {
|
|
186
|
+
r.id = x.id || r.id; r.sealed = true; r.blockIndex = x.block_index; r.merkleRoot = x.merkle_root; counts.sealed++;
|
|
187
|
+
} else { r.error = "not_in_response"; counts.failed++; }
|
|
188
|
+
r._resolve(r);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let lastP = Promise.resolve(null);
|
|
193
|
+
|
|
194
|
+
function enqueue(r) {
|
|
195
|
+
if (!cfg.enabled) { r.error = "disabled"; r._resolve(r); return r; }
|
|
196
|
+
queue.push(r); counts.queued++; kick(400);
|
|
197
|
+
return r;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Seal one action. Resolves to its Receipt (url usable at once; .wait() for the seal). */
|
|
201
|
+
function seal(input, output, label = "", outcome = "ok") {
|
|
202
|
+
const p = (async () => enqueue(new Receipt(label, await fingerprint(input), await fingerprint(output), outcome)))();
|
|
203
|
+
lastP = p;
|
|
204
|
+
p.catch(() => {});
|
|
205
|
+
return p;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Reverse an action you sealed earlier. Nothing is deleted: the original stays
|
|
210
|
+
* sealed and is shown as REVERSED, linked to the new reversal receipt. The
|
|
211
|
+
* reason is public on both. Only whoever sealed the original can reverse it.
|
|
212
|
+
*/
|
|
213
|
+
function reverse(original, reason, output = null, label = "") {
|
|
214
|
+
const oid = typeof original === "string" ? original : original && original.id;
|
|
215
|
+
if (!oid) return Promise.reject(new Error("reverse(original, reason): original receipt or id required"));
|
|
216
|
+
if (!reason) return Promise.reject(new Error("a reversal needs a reason; it is shown on both receipts"));
|
|
217
|
+
const p = (async () => {
|
|
218
|
+
const r = new Receipt(label || "reversal of " + oid, await fingerprint({ reverses: oid }), await fingerprint(output), "reversal");
|
|
219
|
+
r.reverses = oid; r.reason = String(reason).slice(0, 300);
|
|
220
|
+
return enqueue(r);
|
|
221
|
+
})();
|
|
222
|
+
lastP = p;
|
|
223
|
+
return p;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Wrap a function (sync or async). receipt(fn) or receipt("label", fn). */
|
|
227
|
+
function receipt(label, fn) {
|
|
228
|
+
if (typeof label === "function") { fn = label; label = fn.name || "action"; }
|
|
229
|
+
if (typeof fn !== "function") return (f) => receipt(label, f);
|
|
230
|
+
const name = label || fn.name || "action";
|
|
231
|
+
const wrapped = function (...args) {
|
|
232
|
+
const input = args.length === 1 ? args[0] : args;
|
|
233
|
+
let out;
|
|
234
|
+
try { out = fn.apply(this, args); } catch (e) { seal(input, { error_type: (e && e.name) || "Error" }, name, "error"); throw e; }
|
|
235
|
+
if (out && typeof out.then === "function") {
|
|
236
|
+
return out.then((v) => { seal(input, v, name); return v; },
|
|
237
|
+
(e) => { seal(input, { error_type: (e && e.name) || "Error" }, name, "error"); throw e; });
|
|
238
|
+
}
|
|
239
|
+
seal(input, out, name);
|
|
240
|
+
return out;
|
|
241
|
+
};
|
|
242
|
+
Object.defineProperty(wrapped, "name", { value: fn.name });
|
|
243
|
+
return wrapped;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Vercel AI SDK, Mastra, LangChain.js tool objects, MCP handlers: wraps every
|
|
248
|
+
* tool's `execute` (or `func` / `invoke`) so each tool call gets a receipt.
|
|
249
|
+
* tools: wrapTools({ getWeather, sendEmail })
|
|
250
|
+
*/
|
|
251
|
+
function wrapTools(tools, prefix = "") {
|
|
252
|
+
const wrapOne = (name, t) => {
|
|
253
|
+
if (typeof t === "function") return receipt(prefix + name, t);
|
|
254
|
+
if (!t || typeof t !== "object") return t;
|
|
255
|
+
const c = Object.assign(Object.create(Object.getPrototypeOf(t)), t);
|
|
256
|
+
for (const m of ["execute", "func", "invoke"]) {
|
|
257
|
+
if (typeof t[m] === "function") { c[m] = receipt(prefix + (t.name || name), t[m].bind(t)); break; }
|
|
258
|
+
}
|
|
259
|
+
return c;
|
|
260
|
+
};
|
|
261
|
+
if (Array.isArray(tools)) return tools.map((t, i) => wrapOne((t && t.name) || "tool" + i, t));
|
|
262
|
+
const out = {};
|
|
263
|
+
for (const k of Object.keys(tools || {})) out[k] = wrapOne(k, tools[k]);
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Most recent receipt created in this process (a Promise). */
|
|
268
|
+
const last = () => lastP;
|
|
269
|
+
|
|
270
|
+
/** Send everything queued now. */
|
|
271
|
+
async function flush(ms = 5000) {
|
|
272
|
+
const end = Date.now() + ms;
|
|
273
|
+
kick();
|
|
274
|
+
while (Date.now() < end) {
|
|
275
|
+
if (!queue.length && !inflight) return true;
|
|
276
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const stats = () => Object.assign({}, counts, { pending: queue.length, endpoint: cfg.endpoint, keyed: !!cfg.apiKey, notice });
|
|
282
|
+
|
|
283
|
+
/** Check a receipt by id. Anyone can, no key. */
|
|
284
|
+
async function check(id) {
|
|
285
|
+
const res = await fetch(SITE + "/v1/receipts/" + encodeURIComponent(id));
|
|
286
|
+
return res.json();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (typeof process !== "undefined" && typeof process.on === "function") {
|
|
290
|
+
process.on("beforeExit", () => { if (queue.length) kick(); });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { receipt, seal, reverse, wrapTools, last, flush, configure, stats, fingerprint, check, Receipt, VERSION };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const VERSION: string;
|
|
2
|
+
export declare class Receipt {
|
|
3
|
+
id: string; label: string; inputHash: string; outputHash: string; ts: number; outcome: string;
|
|
4
|
+
sealed: boolean; error: string | null; blockIndex: number | null; merkleRoot: string | null; reverses: string | null;
|
|
5
|
+
readonly url: string;
|
|
6
|
+
wait(ms?: number): Promise<Receipt>;
|
|
7
|
+
toJSON(): Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export declare function receipt<F extends (...a: any[]) => any>(fn: F): F;
|
|
10
|
+
export declare function receipt<F extends (...a: any[]) => any>(label: string, fn: F): F;
|
|
11
|
+
export declare function receipt(label: string): <F extends (...a: any[]) => any>(fn: F) => F;
|
|
12
|
+
export declare function seal(input: unknown, output: unknown, label?: string, outcome?: "ok" | "error"): Promise<Receipt>;
|
|
13
|
+
export declare function reverse(original: Receipt | string, reason: string, output?: unknown, label?: string): Promise<Receipt>;
|
|
14
|
+
export declare function wrapTools<T>(tools: T, prefix?: string): T;
|
|
15
|
+
export declare function last(): Promise<Receipt | null>;
|
|
16
|
+
export declare function flush(ms?: number): Promise<boolean>;
|
|
17
|
+
export declare function configure(o: { apiKey?: string; endpoint?: string; enabled?: boolean; client?: string }): void;
|
|
18
|
+
export declare function stats(): { queued: number; sealed: number; failed: number; pending: number; endpoint: string; keyed: boolean; notice: string | null };
|
|
19
|
+
export declare function fingerprint(value: unknown): Promise<string>;
|
|
20
|
+
export declare function check(id: string): Promise<Record<string, any>>;
|
|
21
|
+
declare const _default: {
|
|
22
|
+
receipt: typeof receipt; seal: typeof seal; reverse: typeof reverse; wrapTools: typeof wrapTools; last: typeof last;
|
|
23
|
+
flush: typeof flush; configure: typeof configure; stats: typeof stats; fingerprint: typeof fingerprint; check: typeof check;
|
|
24
|
+
};
|
|
25
|
+
export default _default;
|
package/index.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sebbi",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A receipt for every AI agent action. One line: every tool call gets a sealed, publicly checkable receipt. Vercel AI SDK, LangChain.js, Mastra, MCP.",
|
|
5
|
+
"main": "./index.cjs",
|
|
6
|
+
"module": "./index.mjs",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": { "types": "./index.d.ts", "import": "./index.mjs", "require": "./index.cjs" }
|
|
10
|
+
},
|
|
11
|
+
"files": ["index.cjs", "index.mjs", "index.d.ts", "README.md", "LICENSE"],
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"engines": { "node": ">=18" },
|
|
14
|
+
"keywords": ["ai", "agents", "audit", "receipts", "ai-sdk", "vercel-ai", "langchain", "mastra", "mcp", "governance", "eu-ai-act", "compliance", "provenance", "merkle"],
|
|
15
|
+
"homepage": "https://sebbi.pro/receipts",
|
|
16
|
+
"author": "Monop Content <justrightdecorators@gmail.com>",
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|