@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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { ToolReceiptEvent, Visibility, TraceRun, TraceVerificationResult, TraceBundle } from '@fluxpointstudios/orynq-sdk-process-trace';
|
|
2
|
+
import { KeyObject } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview Helpers to record `tool-receipt` events into a trace (issue #60).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministically hash a request/response payload for the `request.hash` /
|
|
10
|
+
* `response.hash` commitments (canonical JSON, SHA-256 hex). Strings are hashed
|
|
11
|
+
* as-is; everything else is canonicalized first.
|
|
12
|
+
*/
|
|
13
|
+
declare function hashToolPayload(value: unknown): Promise<string>;
|
|
14
|
+
interface AddToolReceiptOptions {
|
|
15
|
+
toolId: string;
|
|
16
|
+
/** Commitment to the request (use {@link hashToolPayload}). */
|
|
17
|
+
request: {
|
|
18
|
+
hash: string;
|
|
19
|
+
};
|
|
20
|
+
/** Commitment to the response, with an optional retained payload. */
|
|
21
|
+
response: {
|
|
22
|
+
hash: string;
|
|
23
|
+
payload?: unknown;
|
|
24
|
+
};
|
|
25
|
+
/** The independently-verifiable signed receipt. */
|
|
26
|
+
receipt: ToolReceiptEvent["receipt"];
|
|
27
|
+
/** Event visibility (default "private" — responses may carry PII). */
|
|
28
|
+
visibility?: Visibility;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Append a `tool-receipt` event to a span.
|
|
32
|
+
*
|
|
33
|
+
* @returns the recorded {@link ToolReceiptEvent} (with runtime fields).
|
|
34
|
+
*/
|
|
35
|
+
declare function addToolReceipt(run: TraceRun, spanId: string, opts: AddToolReceiptOptions): Promise<ToolReceiptEvent>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @fileoverview Signature-scheme verifiers for tool-call receipts (issue #60).
|
|
39
|
+
*
|
|
40
|
+
* Each verifier checks that `receipt.signature` is a valid signature over
|
|
41
|
+
* `receipt.signedPayload` for `receipt.signer`, per its scheme:
|
|
42
|
+
*
|
|
43
|
+
* - `http-message-signatures` — RFC 9421 (signature base provided as signedPayload)
|
|
44
|
+
* - `stripe-webhook` — Stripe `Stripe-Signature` HMAC-SHA256 (timestamped; freshness-checked)
|
|
45
|
+
* - `github-webhook` — GitHub `X-Hub-Signature-256` HMAC-SHA256 (no signed
|
|
46
|
+
* timestamp; freshness-checked only when `params.timestamp` is recorded,
|
|
47
|
+
* otherwise anti-replay rests on the bundle Merkle commitment)
|
|
48
|
+
* - `jws` — compact JWS / JWT (HS*, RS*, PS*, ES*, EdDSA)
|
|
49
|
+
*
|
|
50
|
+
* Symmetric secrets (webhooks, HS*) MUST be supplied out-of-band via the
|
|
51
|
+
* {@link ToolReceiptVerifyContext} — never embedded in the trace. Asymmetric
|
|
52
|
+
* *public* keys may be embedded in `receipt.params.publicKey`.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
56
|
+
/** Key/secret resolution + policy context for receipt verification. */
|
|
57
|
+
interface ToolReceiptVerifyContext {
|
|
58
|
+
/** Verification keys/secrets keyed by `receipt.signer`. */
|
|
59
|
+
keys?: Record<string, string>;
|
|
60
|
+
/** Dynamic key/secret resolver (takes precedence over `keys`). */
|
|
61
|
+
resolveKey?: (event: ToolReceiptEvent) => MaybePromise<string | Uint8Array | undefined>;
|
|
62
|
+
/** Max age (seconds) for replay-protected schemes (Stripe). Default 300. */
|
|
63
|
+
toleranceSec?: number;
|
|
64
|
+
/** Epoch-seconds clock override (testing). */
|
|
65
|
+
nowSec?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Optional per-signer expected-algorithm allow-list (defense-in-depth against
|
|
68
|
+
* algorithm confusion). When present for a signer, any receipt whose resolved
|
|
69
|
+
* `alg` is not listed is rejected — e.g. `{ "tee://oracle": ["EdDSA"] }` pins
|
|
70
|
+
* that signer to EdDSA so an attacker-set `alg:HS256` is refused.
|
|
71
|
+
*/
|
|
72
|
+
keyAlgs?: Record<string, string[]>;
|
|
73
|
+
/**
|
|
74
|
+
* Accept a public key embedded in `receipt.params.publicKey` when no
|
|
75
|
+
* out-of-band key is configured. This is a CONVENIENCE for internal
|
|
76
|
+
* consistency checks only — it is NOT an authenticity guarantee, because the
|
|
77
|
+
* trace (and therefore the embedded key) is attacker-controlled. Default
|
|
78
|
+
* false; supply the signer's key via `keys`/`resolveKey` for a trustworthy
|
|
79
|
+
* verdict.
|
|
80
|
+
*/
|
|
81
|
+
trustEmbeddedKeys?: boolean;
|
|
82
|
+
}
|
|
83
|
+
declare function verifyStripeReceipt(event: ToolReceiptEvent, ctx?: ToolReceiptVerifyContext): Promise<boolean>;
|
|
84
|
+
declare function verifyGitHubReceipt(event: ToolReceiptEvent, ctx?: ToolReceiptVerifyContext): Promise<boolean>;
|
|
85
|
+
declare function verifyJwsReceipt(event: ToolReceiptEvent, ctx?: ToolReceiptVerifyContext): Promise<boolean>;
|
|
86
|
+
declare function verifyHttpMessageReceipt(event: ToolReceiptEvent, ctx?: ToolReceiptVerifyContext): Promise<boolean>;
|
|
87
|
+
/**
|
|
88
|
+
* Read the signed call-binding context ({runId, requestHash}) from a self-signed
|
|
89
|
+
* JWS receipt's header, or `null` when the receipt is not a bound JWS. Because
|
|
90
|
+
* the header is part of the JWS signing input, these values are covered by the
|
|
91
|
+
* signature — lifting the receipt into another trace/request breaks the match.
|
|
92
|
+
*/
|
|
93
|
+
declare function jwsBindingContext(event: ToolReceiptEvent): {
|
|
94
|
+
runId: string;
|
|
95
|
+
requestHash?: string;
|
|
96
|
+
} | null;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @fileoverview Verification dispatch for tool-call receipts (issue #60).
|
|
100
|
+
*
|
|
101
|
+
* `verifyToolReceipts(bundle, ctx)` verifies every `tool-receipt` event in a
|
|
102
|
+
* bundle; `verifyTrace(bundle, ctx)` additionally runs the full process-trace
|
|
103
|
+
* `verifyBundle()` and folds the receipt result into its `checks` so a single
|
|
104
|
+
* call gives auditors "did the trace verify AND did every tool actually return
|
|
105
|
+
* what the wrapper claims".
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/** A verifier for a single receipt scheme. */
|
|
109
|
+
type ToolReceiptSchemeVerifier = (event: ToolReceiptEvent, ctx?: ToolReceiptVerifyContext) => Promise<boolean>;
|
|
110
|
+
/** Built-in verifiers keyed by `receipt.scheme`. */
|
|
111
|
+
declare const BUILTIN_TOOL_RECEIPT_VERIFIERS: Record<string, ToolReceiptSchemeVerifier>;
|
|
112
|
+
/** Per-receipt verification result. */
|
|
113
|
+
interface ToolReceiptVerificationResult {
|
|
114
|
+
eventId: string;
|
|
115
|
+
toolId: string;
|
|
116
|
+
scheme: string;
|
|
117
|
+
signer: string;
|
|
118
|
+
verified: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* True ONLY when the signature cryptographically binds this receipt to THIS
|
|
121
|
+
* trace's runId AND the recorded request hash (self-signed JWS anti-lie path,
|
|
122
|
+
* with a signed `orynqBinding.{runId,requestHash}` that both matched). A JWS
|
|
123
|
+
* with no binding, or one that binds only the runId, is `false` — the request
|
|
124
|
+
* is not authenticated. Always `false` for external webhook schemes
|
|
125
|
+
* (stripe/github/rfc9421): those prove authenticity of the response body but
|
|
126
|
+
* cannot cover our runId/request, so call-binding is not provable by the
|
|
127
|
+
* signature and request-attribution must not be inferred — anti-replay for them
|
|
128
|
+
* relies on the bundle Merkle commitment + the scheme's own timestamp window.
|
|
129
|
+
*/
|
|
130
|
+
callBound: boolean;
|
|
131
|
+
/** When not verified, a short machine-readable reason. */
|
|
132
|
+
reason?: string;
|
|
133
|
+
error?: string;
|
|
134
|
+
}
|
|
135
|
+
/** Context for {@link verifyToolReceipts}; extends key resolution with custom schemes. */
|
|
136
|
+
interface VerifyToolReceiptsContext extends ToolReceiptVerifyContext {
|
|
137
|
+
/** Register or override scheme verifiers (e.g. a custom/internal scheme). */
|
|
138
|
+
verifiers?: Record<string, ToolReceiptSchemeVerifier>;
|
|
139
|
+
/**
|
|
140
|
+
* The enclosing trace's run id. Self-signed JWS receipts commit to it (and to
|
|
141
|
+
* the request hash) so a genuine receipt cannot be lifted into another trace.
|
|
142
|
+
* {@link verifyToolReceipts} supplies it automatically from the bundle.
|
|
143
|
+
*/
|
|
144
|
+
runId?: string;
|
|
145
|
+
/**
|
|
146
|
+
* When true, a receipt whose signature does not provably bind THIS call
|
|
147
|
+
* (runId + request hash) is `verified: false` with reason `call-binding-required`.
|
|
148
|
+
* Use this when request-attribution must be cryptographic: it rejects unbound
|
|
149
|
+
* JWS receipts and all webhook schemes (whose external signatures cannot cover
|
|
150
|
+
* our request). Default false — call-binding is reported via `callBound` but not
|
|
151
|
+
* required.
|
|
152
|
+
*/
|
|
153
|
+
requireCallBinding?: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** Extract all `tool-receipt` events from a bundle (ordered by seq). */
|
|
156
|
+
declare function extractToolReceipts(bundle: TraceBundle): ToolReceiptEvent[];
|
|
157
|
+
/** Verify a single tool-receipt event. Never throws — failures land in `error`. */
|
|
158
|
+
declare function verifyToolReceipt(event: ToolReceiptEvent, ctx?: VerifyToolReceiptsContext): Promise<ToolReceiptVerificationResult>;
|
|
159
|
+
/** Aggregate outcome over all tool receipts in a bundle. */
|
|
160
|
+
interface ToolReceiptsVerifyOutcome {
|
|
161
|
+
valid: boolean;
|
|
162
|
+
errors: string[];
|
|
163
|
+
results: ToolReceiptVerificationResult[];
|
|
164
|
+
}
|
|
165
|
+
/** Verify every tool-receipt in a bundle. A trace with no receipts is `valid: true`. */
|
|
166
|
+
declare function verifyToolReceipts(bundle: TraceBundle, ctx?: VerifyToolReceiptsContext): Promise<ToolReceiptsVerifyOutcome>;
|
|
167
|
+
/** Combined result of {@link verifyTrace}. */
|
|
168
|
+
interface VerifyTraceResult extends TraceVerificationResult {
|
|
169
|
+
toolReceipts: ToolReceiptsVerifyOutcome;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Verify the whole trace AND its tool receipts in one call. The receipt outcome
|
|
173
|
+
* is folded into `checks.toolReceiptsValid` (so `valid` reflects receipts too)
|
|
174
|
+
* and also returned in full under `toolReceipts`.
|
|
175
|
+
*/
|
|
176
|
+
declare function verifyTrace(bundle: TraceBundle, ctx?: VerifyToolReceiptsContext): Promise<VerifyTraceResult>;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @fileoverview Signing-proxy helper — the "anti-lie" pattern (issue #60).
|
|
180
|
+
*
|
|
181
|
+
* Tools that do not sign their responses natively can be wrapped in a TEE/HSM-
|
|
182
|
+
* signed envelope so the wrapper's claim ("the tool returned X") becomes
|
|
183
|
+
* independently verifiable. This helper produces a JWS-signed
|
|
184
|
+
* {@link ToolReceiptEvent.receipt} from a tool response; pair it with
|
|
185
|
+
* `addToolReceipt` to record the receipt, and `verifyToolReceipts` to check it.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* import { generateKeyPairSync } from "node:crypto";
|
|
190
|
+
* const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
191
|
+
* const proxy = createSigningProxy({
|
|
192
|
+
* signer: "tee://pricing-proxy",
|
|
193
|
+
* alg: "EdDSA",
|
|
194
|
+
* privateKey,
|
|
195
|
+
* publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
196
|
+
* });
|
|
197
|
+
* const receipt = proxy.sign({ price: 4200, currency: "usd" });
|
|
198
|
+
* await addToolReceipt(run, span.id, { toolId, request, response, receipt });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
/** JWS algorithms supported by the signing proxy. */
|
|
203
|
+
type JwsAlg = "EdDSA" | "ES256" | "ES384" | "RS256" | "RS512" | "PS256" | "PS512" | "HS256";
|
|
204
|
+
interface SigningProxyOptions {
|
|
205
|
+
/** Verifier-resolvable identity recorded as `receipt.signer` (URL / DID / keyId). */
|
|
206
|
+
signer: string;
|
|
207
|
+
alg: JwsAlg;
|
|
208
|
+
/** Private key (PEM or KeyObject) for asymmetric algorithms. */
|
|
209
|
+
privateKey?: string | KeyObject;
|
|
210
|
+
/** Shared secret for HS256. */
|
|
211
|
+
secret?: string;
|
|
212
|
+
/**
|
|
213
|
+
* Public key (PEM or JWK string) to embed in `receipt.params.publicKey` so a
|
|
214
|
+
* verifier can resolve it without out-of-band material. Safe to embed
|
|
215
|
+
* (public keys only).
|
|
216
|
+
*/
|
|
217
|
+
publicKey?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Call-binding context signed into the receipt header so a genuine receipt
|
|
220
|
+
* cannot be lifted into a different trace/request. Any signed value that
|
|
221
|
+
* disagrees with the enclosing trace is a hard verification failure. To make a
|
|
222
|
+
* receipt `callBound` (request cryptographically attributed), sign BOTH `runId`
|
|
223
|
+
* AND `requestHash` — binding only the runId scopes the trace but leaves the
|
|
224
|
+
* request unauthenticated, so the receipt verifies but is NOT `callBound`.
|
|
225
|
+
*/
|
|
226
|
+
binding?: {
|
|
227
|
+
runId: string;
|
|
228
|
+
requestHash?: string;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
interface SigningProxy {
|
|
232
|
+
/** Wrap a tool response in a JWS-signed receipt ready for `addToolReceipt`. */
|
|
233
|
+
sign(payload: unknown): ToolReceiptEvent["receipt"];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Create a signing proxy that turns unsigned tool responses into verifiable
|
|
237
|
+
* JWS receipts. Use a TEE/HSM-held key in production.
|
|
238
|
+
*/
|
|
239
|
+
declare function createSigningProxy(opts: SigningProxyOptions): SigningProxy;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @summary Main entry point for @fluxpointstudios/orynq-sdk-tool-receipts.
|
|
243
|
+
*
|
|
244
|
+
* Verifiable tool-call receipts for the Orynq process-trace SDK (issue #60).
|
|
245
|
+
* Proves "the tool actually returned this response" — not just "the agent says
|
|
246
|
+
* the tool returned this response" — by independently verifying signed receipts
|
|
247
|
+
* recorded as `tool-receipt` events.
|
|
248
|
+
*
|
|
249
|
+
* Supported schemes: RFC 9421 HTTP Message Signatures, Stripe/GitHub webhook
|
|
250
|
+
* signatures, and generic JWS. Plus a signing-proxy helper to wrap tools that
|
|
251
|
+
* don't sign their responses natively (the "anti-lie" pattern).
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* import { addToolReceipt, hashToolPayload, verifyTrace } from "@fluxpointstudios/orynq-sdk-tool-receipts";
|
|
256
|
+
*
|
|
257
|
+
* await addToolReceipt(run, span.id, {
|
|
258
|
+
* toolId: "stripe.charges.create",
|
|
259
|
+
* request: { hash: await hashToolPayload(req) },
|
|
260
|
+
* response: { hash: await hashToolPayload(res), payload: res },
|
|
261
|
+
* receipt: { scheme: "stripe-webhook", signer: "acct_123", signature: sigHeader, signedPayload: rawBody },
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* const result = await verifyTrace(bundle, { keys: { acct_123: process.env.STRIPE_WEBHOOK_SECRET! } });
|
|
265
|
+
* // result.valid && result.toolReceipts.results[0].verified
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/** Package version. */
|
|
270
|
+
declare const VERSION = "0.1.0";
|
|
271
|
+
|
|
272
|
+
export { type AddToolReceiptOptions, BUILTIN_TOOL_RECEIPT_VERIFIERS, type JwsAlg, type SigningProxy, type SigningProxyOptions, type ToolReceiptSchemeVerifier, type ToolReceiptVerificationResult, type ToolReceiptVerifyContext, type ToolReceiptsVerifyOutcome, VERSION, type VerifyToolReceiptsContext, type VerifyTraceResult, addToolReceipt, createSigningProxy, extractToolReceipts, hashToolPayload, jwsBindingContext, verifyGitHubReceipt, verifyHttpMessageReceipt, verifyJwsReceipt, verifyStripeReceipt, verifyToolReceipt, verifyToolReceipts, verifyTrace };
|