@fluxpointstudios/orynq-sdk-tool-receipts 0.2.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/dist/index.cjs +518 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +272 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +503 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/__tests__/hardening-round2.test.ts +233 -0
- package/src/__tests__/hardening-round3.test.ts +93 -0
- package/src/__tests__/hardening-round4.test.ts +265 -0
- package/src/__tests__/tool-receipts.test.ts +296 -0
- package/src/index.ts +64 -0
- package/src/record.ts +60 -0
- package/src/schemes.ts +542 -0
- package/src/signing-proxy.ts +145 -0
- package/src/verify.ts +245 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @summary Tests for verifiable tool-call receipts (issue #60).
|
|
3
|
+
*
|
|
4
|
+
* A receipt only counts as verified when BOTH hold:
|
|
5
|
+
* 1. the signature is valid over `receipt.signedPayload`, AND
|
|
6
|
+
* 2. the signed content commits to the recorded `response.hash`, verified with
|
|
7
|
+
* an out-of-band (or explicitly trusted) key.
|
|
8
|
+
* The forgery suites below pin those two properties.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
createHash,
|
|
14
|
+
createHmac,
|
|
15
|
+
generateKeyPairSync,
|
|
16
|
+
sign as cryptoSign,
|
|
17
|
+
} from "node:crypto";
|
|
18
|
+
import {
|
|
19
|
+
createTrace,
|
|
20
|
+
addSpan,
|
|
21
|
+
closeSpan,
|
|
22
|
+
finalizeTrace,
|
|
23
|
+
} from "@fluxpointstudios/orynq-sdk-process-trace";
|
|
24
|
+
import type { TraceRun, TraceBundle, ToolReceiptEvent } from "@fluxpointstudios/orynq-sdk-process-trace";
|
|
25
|
+
import {
|
|
26
|
+
addToolReceipt,
|
|
27
|
+
hashToolPayload,
|
|
28
|
+
verifyToolReceipts,
|
|
29
|
+
verifyTrace,
|
|
30
|
+
createSigningProxy,
|
|
31
|
+
} from "../index.js";
|
|
32
|
+
|
|
33
|
+
/** Record a receipt whose recorded response is `responsePayload`. */
|
|
34
|
+
async function traceWithReceipt(
|
|
35
|
+
receipt: ToolReceiptEvent["receipt"],
|
|
36
|
+
responsePayload: unknown
|
|
37
|
+
): Promise<TraceBundle> {
|
|
38
|
+
const run: TraceRun = await createTrace({ agentId: "agent-1" });
|
|
39
|
+
const span = addSpan(run, { name: "tool-call", visibility: "public" });
|
|
40
|
+
await addToolReceipt(run, span.id, {
|
|
41
|
+
toolId: "demo.tool",
|
|
42
|
+
request: { hash: await hashToolPayload({ q: 1 }) },
|
|
43
|
+
response: { hash: await hashToolPayload(responsePayload), payload: responsePayload },
|
|
44
|
+
receipt,
|
|
45
|
+
});
|
|
46
|
+
await closeSpan(run, span.id);
|
|
47
|
+
return finalizeTrace(run);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pem = (k: ReturnType<typeof generateKeyPairSync>["publicKey"]): string =>
|
|
51
|
+
k.export({ type: "spki", format: "pem" }).toString();
|
|
52
|
+
|
|
53
|
+
describe("Stripe webhook receipts", () => {
|
|
54
|
+
const secret = "whsec_test_secret";
|
|
55
|
+
const body = JSON.stringify({ id: "evt_1", type: "charge.succeeded" });
|
|
56
|
+
|
|
57
|
+
it("verifies a valid Stripe signature within tolerance (response bound to the body)", async () => {
|
|
58
|
+
const t = 1_900_000_000;
|
|
59
|
+
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
|
|
60
|
+
const bundle = await traceWithReceipt(
|
|
61
|
+
{ scheme: "stripe-webhook", signer: "acct_123", signature: `t=${t},v1=${v1}`, signedPayload: body },
|
|
62
|
+
body
|
|
63
|
+
);
|
|
64
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { acct_123: secret }, nowSec: t + 10 });
|
|
65
|
+
expect(outcome.valid).toBe(true);
|
|
66
|
+
expect(outcome.results[0]!.verified).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("rejects a stale Stripe signature (outside tolerance)", async () => {
|
|
70
|
+
const t = 1_900_000_000;
|
|
71
|
+
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
|
|
72
|
+
const bundle = await traceWithReceipt(
|
|
73
|
+
{ scheme: "stripe-webhook", signer: "acct_123", signature: `t=${t},v1=${v1}`, signedPayload: body },
|
|
74
|
+
body
|
|
75
|
+
);
|
|
76
|
+
const outcome = await verifyToolReceipts(bundle, {
|
|
77
|
+
keys: { acct_123: secret },
|
|
78
|
+
nowSec: t + 10_000,
|
|
79
|
+
toleranceSec: 300,
|
|
80
|
+
});
|
|
81
|
+
expect(outcome.valid).toBe(false);
|
|
82
|
+
expect(outcome.results[0]!.error).toMatch(/tolerance/i);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("rejects a wrong secret", async () => {
|
|
86
|
+
const t = 1_900_000_000;
|
|
87
|
+
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
|
|
88
|
+
const bundle = await traceWithReceipt(
|
|
89
|
+
{ scheme: "stripe-webhook", signer: "acct_123", signature: `t=${t},v1=${v1}`, signedPayload: body },
|
|
90
|
+
body
|
|
91
|
+
);
|
|
92
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { acct_123: "whsec_wrong" }, nowSec: t + 10 });
|
|
93
|
+
expect(outcome.results[0]!.verified).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("GitHub webhook receipts", () => {
|
|
98
|
+
const secret = "gh_webhook_secret";
|
|
99
|
+
const body = JSON.stringify({ action: "opened", number: 42 });
|
|
100
|
+
|
|
101
|
+
it("verifies a valid sha256= signature bound to the body", async () => {
|
|
102
|
+
const sig = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
|
|
103
|
+
const bundle = await traceWithReceipt(
|
|
104
|
+
{ scheme: "github-webhook", signer: "gh:org/repo", signature: sig, signedPayload: body },
|
|
105
|
+
body
|
|
106
|
+
);
|
|
107
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "gh:org/repo": secret } });
|
|
108
|
+
expect(outcome.results[0]!.verified).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("rejects a tampered body", async () => {
|
|
112
|
+
const sig = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
|
|
113
|
+
const bundle = await traceWithReceipt(
|
|
114
|
+
{ scheme: "github-webhook", signer: "gh:org/repo", signature: sig, signedPayload: body + "tampered" },
|
|
115
|
+
body
|
|
116
|
+
);
|
|
117
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "gh:org/repo": secret } });
|
|
118
|
+
expect(outcome.results[0]!.verified).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("RFC 9421 HTTP Message Signatures", () => {
|
|
123
|
+
it("verifies an ed25519 signature whose base binds the body via content-digest", async () => {
|
|
124
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
125
|
+
const body = JSON.stringify({ charged: true });
|
|
126
|
+
const cd = createHash("sha256").update(body).digest("base64");
|
|
127
|
+
const signatureBase =
|
|
128
|
+
`"@method": POST\n"@path": /charge\n"content-digest": sha-256=:${cd}:\n` +
|
|
129
|
+
`"@signature-params": ("@method" "@path" "content-digest");created=1900000000`;
|
|
130
|
+
const sig = cryptoSign(null, Buffer.from(signatureBase), privateKey).toString("base64");
|
|
131
|
+
const bundle = await traceWithReceipt(
|
|
132
|
+
{
|
|
133
|
+
scheme: "http-message-signatures",
|
|
134
|
+
signer: "key-ed25519",
|
|
135
|
+
signature: sig,
|
|
136
|
+
signedPayload: signatureBase,
|
|
137
|
+
params: { alg: "ed25519", publicKey: pem(publicKey) },
|
|
138
|
+
},
|
|
139
|
+
body
|
|
140
|
+
);
|
|
141
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "key-ed25519": pem(publicKey) } });
|
|
142
|
+
expect(outcome.results[0]!.verified).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects an RFC 9421 receipt whose base has no content-digest (proves nothing about the response)", async () => {
|
|
146
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
147
|
+
const body = JSON.stringify({ charged: true });
|
|
148
|
+
const signatureBase = '"@method": POST\n"@signature-params": ("@method");created=1900000000';
|
|
149
|
+
const sig = cryptoSign(null, Buffer.from(signatureBase), privateKey).toString("base64");
|
|
150
|
+
const bundle = await traceWithReceipt(
|
|
151
|
+
{
|
|
152
|
+
scheme: "http-message-signatures",
|
|
153
|
+
signer: "key-ed25519",
|
|
154
|
+
signature: sig,
|
|
155
|
+
signedPayload: signatureBase,
|
|
156
|
+
params: { alg: "ed25519", publicKey: pem(publicKey) },
|
|
157
|
+
},
|
|
158
|
+
body
|
|
159
|
+
);
|
|
160
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "key-ed25519": pem(publicKey) } });
|
|
161
|
+
expect(outcome.results[0]!.verified).toBe(false);
|
|
162
|
+
expect(outcome.results[0]!.reason).toBe("response-not-bound");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("JWS receipts via signing proxy (anti-lie pattern)", () => {
|
|
167
|
+
it("signs + verifies an EdDSA receipt with an out-of-band key", async () => {
|
|
168
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
169
|
+
const proxy = createSigningProxy({ signer: "tee://pricing-proxy", alg: "EdDSA", privateKey, publicKey: pem(publicKey) });
|
|
170
|
+
const payload = { price: 4200, currency: "usd" };
|
|
171
|
+
const receipt = proxy.sign(payload);
|
|
172
|
+
expect(receipt.scheme).toBe("jws");
|
|
173
|
+
const bundle = await traceWithReceipt(receipt, payload);
|
|
174
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "tee://pricing-proxy": pem(publicKey) } });
|
|
175
|
+
expect(outcome.results[0]!.verified).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("signs + verifies an RS256 receipt", async () => {
|
|
179
|
+
const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
|
|
180
|
+
const proxy = createSigningProxy({ signer: "proxy-rsa", alg: "RS256", privateKey, publicKey: pem(publicKey) });
|
|
181
|
+
const payload = { data: "x" };
|
|
182
|
+
const bundle = await traceWithReceipt(proxy.sign(payload), payload);
|
|
183
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "proxy-rsa": pem(publicKey) } });
|
|
184
|
+
expect(outcome.results[0]!.verified).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("HS256 secret is NOT embedded and must be supplied to verify", async () => {
|
|
188
|
+
const proxy = createSigningProxy({ signer: "hs-signer", alg: "HS256", secret: "topsecret" });
|
|
189
|
+
const payload = { data: "y" };
|
|
190
|
+
const bundle = await traceWithReceipt(proxy.sign(payload), payload);
|
|
191
|
+
const noKey = await verifyToolReceipts(bundle);
|
|
192
|
+
expect(noKey.results[0]!.verified).toBe(false);
|
|
193
|
+
const withKey = await verifyToolReceipts(bundle, { keys: { "hs-signer": "topsecret" } });
|
|
194
|
+
expect(withKey.results[0]!.verified).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("flags a tampered JWS signature", async () => {
|
|
198
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
199
|
+
const proxy = createSigningProxy({ signer: "tee://x", alg: "EdDSA", privateKey, publicKey: pem(publicKey) });
|
|
200
|
+
const payload = { a: 1 };
|
|
201
|
+
const receipt = proxy.sign(payload);
|
|
202
|
+
// Deterministic tamper: flip a byte in the DECODED signature and re-encode.
|
|
203
|
+
// A base64url character swap can decode to identical bytes for a 64-byte
|
|
204
|
+
// ed25519 sig, so tamper at the byte level to reliably corrupt it.
|
|
205
|
+
const sigBytes = Buffer.from(receipt.signature, "base64url");
|
|
206
|
+
sigBytes[Math.floor(sigBytes.length / 2)] ^= 0xff;
|
|
207
|
+
receipt.signature = sigBytes.toString("base64url");
|
|
208
|
+
const bundle = await traceWithReceipt(receipt, payload);
|
|
209
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "tee://x": pem(publicKey) } });
|
|
210
|
+
expect(outcome.results[0]!.verified).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("Forgery resistance (issue #60 core guarantee)", () => {
|
|
215
|
+
it("rejects a genuine signature paired with a fabricated response (response-swap)", async () => {
|
|
216
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
217
|
+
const proxy = createSigningProxy({ signer: "tee://oracle", alg: "EdDSA", privateKey, publicKey: pem(publicKey) });
|
|
218
|
+
// The TEE honestly signs price:100 ...
|
|
219
|
+
const receipt = proxy.sign({ price: 100, currency: "usd" });
|
|
220
|
+
// ... but the agent records price:1 next to the genuine receipt.
|
|
221
|
+
const bundle = await traceWithReceipt(receipt, { price: 1, currency: "usd" });
|
|
222
|
+
const outcome = await verifyToolReceipts(bundle, { keys: { "tee://oracle": pem(publicKey) } });
|
|
223
|
+
expect(outcome.results[0]!.verified).toBe(false);
|
|
224
|
+
expect(outcome.results[0]!.reason).toBe("response-binding-mismatch");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("rejects an attacker-embedded key claiming a trusted signer (embedded key not trusted by default)", async () => {
|
|
228
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
229
|
+
const proxy = createSigningProxy({
|
|
230
|
+
signer: "tee://trusted-oracle",
|
|
231
|
+
alg: "EdDSA",
|
|
232
|
+
privateKey, // attacker's own key
|
|
233
|
+
publicKey: pem(publicKey), // attacker embeds the matching pubkey
|
|
234
|
+
});
|
|
235
|
+
const payload = { price: 999_999, note: "FORGED" };
|
|
236
|
+
const receipt = proxy.sign(payload); // response binding matches — attacker controls both
|
|
237
|
+
const bundle = await traceWithReceipt(receipt, payload);
|
|
238
|
+
|
|
239
|
+
// Default: embedded key is untrusted -> not verified.
|
|
240
|
+
const def = await verifyToolReceipts(bundle);
|
|
241
|
+
expect(def.results[0]!.verified).toBe(false);
|
|
242
|
+
|
|
243
|
+
// An auditor who pins the REAL oracle key rejects the forged signature.
|
|
244
|
+
const { publicKey: realPub } = generateKeyPairSync("ed25519");
|
|
245
|
+
const pinned = await verifyToolReceipts(bundle, { keys: { "tee://trusted-oracle": pem(realPub) } });
|
|
246
|
+
expect(pinned.results[0]!.verified).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("accepts an embedded key ONLY when the caller explicitly opts in", async () => {
|
|
250
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
251
|
+
const proxy = createSigningProxy({ signer: "tee://p", alg: "EdDSA", privateKey, publicKey: pem(publicKey) });
|
|
252
|
+
const payload = { ok: true };
|
|
253
|
+
const bundle = await traceWithReceipt(proxy.sign(payload), payload);
|
|
254
|
+
const outcome = await verifyToolReceipts(bundle, { trustEmbeddedKeys: true });
|
|
255
|
+
expect(outcome.results[0]!.verified).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("verifyTrace integration", () => {
|
|
260
|
+
it("folds receipt verification into the bundle result", async () => {
|
|
261
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
262
|
+
const proxy = createSigningProxy({ signer: "tee://ok", alg: "EdDSA", privateKey, publicKey: pem(publicKey) });
|
|
263
|
+
const payload = { ok: true };
|
|
264
|
+
const bundle = await traceWithReceipt(proxy.sign(payload), payload);
|
|
265
|
+
|
|
266
|
+
const result = await verifyTrace(bundle, { keys: { "tee://ok": pem(publicKey) } });
|
|
267
|
+
expect(result.valid).toBe(true);
|
|
268
|
+
expect(result.checks.toolReceiptsValid).toBe(true);
|
|
269
|
+
expect(result.toolReceipts.results[0]!.verified).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("fails the whole trace when a receipt fails", async () => {
|
|
273
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
274
|
+
const proxy = createSigningProxy({ signer: "tee://bad", alg: "EdDSA", privateKey, publicKey: pem(publicKey) });
|
|
275
|
+
const payload = { ok: true };
|
|
276
|
+
const receipt = proxy.sign(payload);
|
|
277
|
+
const sigBytes = Buffer.from(receipt.signature, "base64url");
|
|
278
|
+
sigBytes[Math.floor(sigBytes.length / 2)] ^= 0xff;
|
|
279
|
+
receipt.signature = sigBytes.toString("base64url");
|
|
280
|
+
const bundle = await traceWithReceipt(receipt, payload);
|
|
281
|
+
|
|
282
|
+
const result = await verifyTrace(bundle, { keys: { "tee://bad": pem(publicKey) } });
|
|
283
|
+
expect(result.checks.toolReceiptsValid).toBe(false);
|
|
284
|
+
expect(result.valid).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("reports unknown schemes as unverified", async () => {
|
|
288
|
+
const bundle = await traceWithReceipt(
|
|
289
|
+
{ scheme: "exotic-scheme", signer: "x", signature: "deadbeef", signedPayload: "{}" },
|
|
290
|
+
{}
|
|
291
|
+
);
|
|
292
|
+
const outcome = await verifyToolReceipts(bundle);
|
|
293
|
+
expect(outcome.results[0]!.verified).toBe(false);
|
|
294
|
+
expect(outcome.results[0]!.error).toMatch(/no verifier/i);
|
|
295
|
+
});
|
|
296
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @summary Main entry point for @fluxpointstudios/orynq-sdk-tool-receipts.
|
|
3
|
+
*
|
|
4
|
+
* Verifiable tool-call receipts for the Orynq process-trace SDK (issue #60).
|
|
5
|
+
* Proves "the tool actually returned this response" — not just "the agent says
|
|
6
|
+
* the tool returned this response" — by independently verifying signed receipts
|
|
7
|
+
* recorded as `tool-receipt` events.
|
|
8
|
+
*
|
|
9
|
+
* Supported schemes: RFC 9421 HTTP Message Signatures, Stripe/GitHub webhook
|
|
10
|
+
* signatures, and generic JWS. Plus a signing-proxy helper to wrap tools that
|
|
11
|
+
* don't sign their responses natively (the "anti-lie" pattern).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { addToolReceipt, hashToolPayload, verifyTrace } from "@fluxpointstudios/orynq-sdk-tool-receipts";
|
|
16
|
+
*
|
|
17
|
+
* await addToolReceipt(run, span.id, {
|
|
18
|
+
* toolId: "stripe.charges.create",
|
|
19
|
+
* request: { hash: await hashToolPayload(req) },
|
|
20
|
+
* response: { hash: await hashToolPayload(res), payload: res },
|
|
21
|
+
* receipt: { scheme: "stripe-webhook", signer: "acct_123", signature: sigHeader, signedPayload: rawBody },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const result = await verifyTrace(bundle, { keys: { acct_123: process.env.STRIPE_WEBHOOK_SECRET! } });
|
|
25
|
+
* // result.valid && result.toolReceipts.results[0].verified
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Recording helpers
|
|
30
|
+
export { addToolReceipt, hashToolPayload } from "./record.js";
|
|
31
|
+
export type { AddToolReceiptOptions } from "./record.js";
|
|
32
|
+
|
|
33
|
+
// Scheme verifiers + key-resolution context
|
|
34
|
+
export {
|
|
35
|
+
verifyStripeReceipt,
|
|
36
|
+
verifyGitHubReceipt,
|
|
37
|
+
verifyJwsReceipt,
|
|
38
|
+
verifyHttpMessageReceipt,
|
|
39
|
+
jwsBindingContext,
|
|
40
|
+
} from "./schemes.js";
|
|
41
|
+
export type { ToolReceiptVerifyContext } from "./schemes.js";
|
|
42
|
+
|
|
43
|
+
// Verification dispatch
|
|
44
|
+
export {
|
|
45
|
+
BUILTIN_TOOL_RECEIPT_VERIFIERS,
|
|
46
|
+
extractToolReceipts,
|
|
47
|
+
verifyToolReceipt,
|
|
48
|
+
verifyToolReceipts,
|
|
49
|
+
verifyTrace,
|
|
50
|
+
} from "./verify.js";
|
|
51
|
+
export type {
|
|
52
|
+
ToolReceiptSchemeVerifier,
|
|
53
|
+
ToolReceiptVerificationResult,
|
|
54
|
+
VerifyToolReceiptsContext,
|
|
55
|
+
ToolReceiptsVerifyOutcome,
|
|
56
|
+
VerifyTraceResult,
|
|
57
|
+
} from "./verify.js";
|
|
58
|
+
|
|
59
|
+
// Signing-proxy (anti-lie pattern)
|
|
60
|
+
export { createSigningProxy } from "./signing-proxy.js";
|
|
61
|
+
export type { SigningProxy, SigningProxyOptions, JwsAlg } from "./signing-proxy.js";
|
|
62
|
+
|
|
63
|
+
/** Package version. */
|
|
64
|
+
export const VERSION = "0.1.0";
|
package/src/record.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Helpers to record `tool-receipt` events into a trace (issue #60).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
TraceRun,
|
|
7
|
+
TraceEvent,
|
|
8
|
+
ToolReceiptEvent,
|
|
9
|
+
Visibility,
|
|
10
|
+
} from "@fluxpointstudios/orynq-sdk-process-trace";
|
|
11
|
+
import { addEvent } from "@fluxpointstudios/orynq-sdk-process-trace";
|
|
12
|
+
import { sha256StringHex, canonicalize } from "@fluxpointstudios/orynq-sdk-core/utils";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Deterministically hash a request/response payload for the `request.hash` /
|
|
16
|
+
* `response.hash` commitments (canonical JSON, SHA-256 hex). Strings are hashed
|
|
17
|
+
* as-is; everything else is canonicalized first.
|
|
18
|
+
*/
|
|
19
|
+
export async function hashToolPayload(value: unknown): Promise<string> {
|
|
20
|
+
const serialized = typeof value === "string" ? value : canonicalize(value);
|
|
21
|
+
return sha256StringHex(serialized);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AddToolReceiptOptions {
|
|
25
|
+
toolId: string;
|
|
26
|
+
/** Commitment to the request (use {@link hashToolPayload}). */
|
|
27
|
+
request: { hash: string };
|
|
28
|
+
/** Commitment to the response, with an optional retained payload. */
|
|
29
|
+
response: { hash: string; payload?: unknown };
|
|
30
|
+
/** The independently-verifiable signed receipt. */
|
|
31
|
+
receipt: ToolReceiptEvent["receipt"];
|
|
32
|
+
/** Event visibility (default "private" — responses may carry PII). */
|
|
33
|
+
visibility?: Visibility;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Append a `tool-receipt` event to a span.
|
|
38
|
+
*
|
|
39
|
+
* @returns the recorded {@link ToolReceiptEvent} (with runtime fields).
|
|
40
|
+
*/
|
|
41
|
+
export async function addToolReceipt(
|
|
42
|
+
run: TraceRun,
|
|
43
|
+
spanId: string,
|
|
44
|
+
opts: AddToolReceiptOptions
|
|
45
|
+
): Promise<ToolReceiptEvent> {
|
|
46
|
+
if (!opts.toolId) throw new Error("addToolReceipt: toolId is required");
|
|
47
|
+
if (!opts.receipt) throw new Error("addToolReceipt: receipt is required");
|
|
48
|
+
|
|
49
|
+
const event: Omit<ToolReceiptEvent, "id" | "seq" | "timestamp" | "hash"> = {
|
|
50
|
+
kind: "tool-receipt",
|
|
51
|
+
visibility: opts.visibility ?? "private",
|
|
52
|
+
toolId: opts.toolId,
|
|
53
|
+
request: opts.request,
|
|
54
|
+
response: opts.response,
|
|
55
|
+
receipt: opts.receipt,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const recorded: TraceEvent = await addEvent(run, spanId, event);
|
|
59
|
+
return recorded as ToolReceiptEvent;
|
|
60
|
+
}
|