@absol-labs/agent 0.3.2 → 0.4.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/README.md +215 -169
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/zktls/reclaim.d.ts +84 -5
- package/dist/zktls/reclaim.d.ts.map +1 -1
- package/dist/zktls/reclaim.js +47 -3
- package/dist/zktls/reclaim.js.map +1 -1
- package/dist/zktls/t2-delivery-proof.d.ts +296 -0
- package/dist/zktls/t2-delivery-proof.d.ts.map +1 -0
- package/dist/zktls/t2-delivery-proof.js +336 -0
- package/dist/zktls/t2-delivery-proof.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +22 -0
- package/src/zktls/reclaim.ts +88 -8
- package/src/zktls/t2-delivery-proof.ts +559 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
deliveryReceiptSchema,
|
|
4
|
+
signDeliveryReceipt,
|
|
5
|
+
type DeliveryReceipt,
|
|
6
|
+
type DeliveryReceiptDomainInput,
|
|
7
|
+
} from "@absol-labs/shared";
|
|
8
|
+
import {
|
|
9
|
+
getHttpProviderClaimParamsFromProof,
|
|
10
|
+
getProviderHashRequirementsFromSpec,
|
|
11
|
+
verifyProof,
|
|
12
|
+
} from "@reclaimprotocol/js-sdk";
|
|
13
|
+
import {
|
|
14
|
+
keccak256,
|
|
15
|
+
stringToBytes,
|
|
16
|
+
type Address,
|
|
17
|
+
type Hex,
|
|
18
|
+
type LocalAccount,
|
|
19
|
+
} from "viem";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
asErrorMessage,
|
|
24
|
+
buildDeliveryContextEnvelopeV2,
|
|
25
|
+
defaultReclaimClientFactory,
|
|
26
|
+
deliveryContextEnvelopeV2Schema,
|
|
27
|
+
parseReclaimProofEnv,
|
|
28
|
+
ResponseProofUnavailableError,
|
|
29
|
+
ResponseProofVerificationError,
|
|
30
|
+
toRequestSpec,
|
|
31
|
+
type DeliveryContextEnvelopeV2,
|
|
32
|
+
type HttpsResponseMatch,
|
|
33
|
+
type HttpsResponseRedaction,
|
|
34
|
+
type ReclaimClientLike,
|
|
35
|
+
type ReclaimProof,
|
|
36
|
+
type ReclaimProofServiceConfig,
|
|
37
|
+
type VerifiedConsumedHttpsResponseProof,
|
|
38
|
+
type VerifyProofFn,
|
|
39
|
+
} from "./reclaim.js";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* T2 consumer zkTLS delivery envelope (issue #24, metrik-protocol#67 DECISION).
|
|
43
|
+
*
|
|
44
|
+
* This is the BUYER-side counterpart to `streamproof-oracle`'s T2 accrue-on-proof
|
|
45
|
+
* ingest (`src/delivery/proof-ingest.ts`). Given the stream identity, an
|
|
46
|
+
* oracle-issued nonce (`GET /delivery/nonce`), and the paid-route request spec,
|
|
47
|
+
* it: (1) proves the paid route via a pluggable zkTLS attestor with the nonce
|
|
48
|
+
* bound into the request and the Metrik v2 envelope bound into
|
|
49
|
+
* `claimData.context`; (2) signs a `DeliveryReceipt` (`@absol-labs/shared`)
|
|
50
|
+
* committing to that proof; and (3) returns the exact `POST /delivery/proof`
|
|
51
|
+
* request body the oracle's `deliveryProofRequestSchema` expects.
|
|
52
|
+
*
|
|
53
|
+
* The attestor is pluggable (`DeliveryProofAttestor`) specifically so this can
|
|
54
|
+
* be tested WITHOUT a live Reclaim App Secret (agent#22 is the human-intervention
|
|
55
|
+
* issue for that credential) - the default production attestor
|
|
56
|
+
* (`createReclaimT2AttestorFromEnv`) still requires it.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
const bytes32Hex = /^0x[0-9a-fA-F]{64}$/;
|
|
60
|
+
const evmAddress = /^0x[0-9a-fA-F]{40}$/;
|
|
61
|
+
const hexSignature = /^0x[0-9a-fA-F]+$/;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* WHERE the oracle-issued nonce is injected into the outbound paid-route
|
|
65
|
+
* request. T2 disallows header injection (`proof-ingest.ts` rejects it) since
|
|
66
|
+
* a header is not part of what the Reclaim proof's `claimData.parameters`
|
|
67
|
+
* commits to - the oracle can only verify a nonce carried in the URL query or
|
|
68
|
+
* the request body.
|
|
69
|
+
*/
|
|
70
|
+
export type T2NonceInjection =
|
|
71
|
+
| { readonly in: "query"; readonly name: string }
|
|
72
|
+
| { readonly in: "body"; readonly placeholder: string };
|
|
73
|
+
|
|
74
|
+
export interface PaidRouteRequestSpec {
|
|
75
|
+
readonly url: string;
|
|
76
|
+
readonly method?: "GET" | "POST" | "PUT";
|
|
77
|
+
readonly headers?: Record<string, string>;
|
|
78
|
+
/** For `nonceInjection.in === "body"`, MUST contain `nonceInjection.placeholder`. */
|
|
79
|
+
readonly body?: string;
|
|
80
|
+
readonly secretHeaders?: Record<string, string>;
|
|
81
|
+
readonly responseMatches: readonly HttpsResponseMatch[];
|
|
82
|
+
readonly responseRedactions?: readonly HttpsResponseRedaction[];
|
|
83
|
+
readonly cookie?: string;
|
|
84
|
+
readonly secretParamValues?: Record<string, string>;
|
|
85
|
+
readonly geoLocation?: string;
|
|
86
|
+
readonly useTee?: boolean;
|
|
87
|
+
readonly zkEngine?: "snarkjs" | "stwo";
|
|
88
|
+
readonly nonceInjection: T2NonceInjection;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class PaidRouteNonceInjectionError extends Error {
|
|
92
|
+
constructor(message: string) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "PaidRouteNonceInjectionError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Injects the oracle-issued nonce into the paid-route request (query or body only). */
|
|
99
|
+
function injectNonce(
|
|
100
|
+
spec: PaidRouteRequestSpec,
|
|
101
|
+
nonce: `0x${string}`,
|
|
102
|
+
): { readonly url: string; readonly body: string | undefined } {
|
|
103
|
+
if (spec.nonceInjection.in === "query") {
|
|
104
|
+
let url: URL;
|
|
105
|
+
try {
|
|
106
|
+
url = new URL(spec.url);
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
throw new PaidRouteNonceInjectionError(
|
|
109
|
+
`paid route url is not a valid absolute URL: ${asErrorMessage(cause)}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
url.searchParams.set(spec.nonceInjection.name, nonce);
|
|
113
|
+
return { url: url.toString(), body: spec.body };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const placeholder = spec.nonceInjection.placeholder;
|
|
117
|
+
const body = spec.body ?? "";
|
|
118
|
+
if (!body.includes(placeholder)) {
|
|
119
|
+
throw new PaidRouteNonceInjectionError(
|
|
120
|
+
`paid route body does not contain the nonce placeholder "${placeholder}"`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return { url: spec.url, body: body.split(placeholder).join(nonce) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Input to a `DeliveryProofAttestor`. `context` becomes `claimData.context`
|
|
128
|
+
* VERBATIM (see `ReclaimClientLike.zkFetch`'s doc in `reclaim.ts`) - it is the
|
|
129
|
+
* flat T2 envelope, never wrapped.
|
|
130
|
+
*/
|
|
131
|
+
export interface ProveDeliveryResponseInput {
|
|
132
|
+
readonly url: string;
|
|
133
|
+
readonly method?: "GET" | "POST" | "PUT" | undefined;
|
|
134
|
+
readonly headers?: Record<string, string> | undefined;
|
|
135
|
+
readonly body?: string | undefined;
|
|
136
|
+
readonly secretHeaders?: Record<string, string> | undefined;
|
|
137
|
+
readonly responseMatches: readonly HttpsResponseMatch[];
|
|
138
|
+
readonly responseRedactions?: readonly HttpsResponseRedaction[] | undefined;
|
|
139
|
+
readonly cookie?: string | undefined;
|
|
140
|
+
readonly secretParamValues?: Record<string, string> | undefined;
|
|
141
|
+
readonly geoLocation?: string | undefined;
|
|
142
|
+
readonly useTee?: boolean | undefined;
|
|
143
|
+
readonly zkEngine?: "snarkjs" | "stwo" | undefined;
|
|
144
|
+
readonly context: DeliveryContextEnvelopeV2;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Pluggable zkTLS attestor abstraction for the T2 delivery proof. The default
|
|
149
|
+
* (`createReclaimT2AttestorFromEnv`) drives the real Reclaim zkFetch + verify
|
|
150
|
+
* pipeline; tests inject a mock/real-crypto-fixture implementation instead so
|
|
151
|
+
* this module is testable without a live `RECLAIM_APP_SECRET` (agent#22).
|
|
152
|
+
*/
|
|
153
|
+
export interface DeliveryProofAttestor {
|
|
154
|
+
proveDeliveryResponse(
|
|
155
|
+
input: ProveDeliveryResponseInput,
|
|
156
|
+
): Promise<VerifiedConsumedHttpsResponseProof>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const reclaimT2UrlSchema = z.string().url();
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Production `DeliveryProofAttestor`: drives the real Reclaim zkFetch + proof
|
|
163
|
+
* verification pipeline (mirrors `ReclaimConsumerProofService` in `reclaim.ts`,
|
|
164
|
+
* but binds the FLAT T2 v2 envelope directly as `context` instead of the v1
|
|
165
|
+
* `{contextAddress, contextMessage}` wrapper).
|
|
166
|
+
*/
|
|
167
|
+
export class ReclaimDeliveryProofAttestor implements DeliveryProofAttestor {
|
|
168
|
+
private readonly client: ReclaimClientLike;
|
|
169
|
+
private readonly config: ReclaimProofServiceConfig;
|
|
170
|
+
private readonly verifyProofImpl: VerifyProofFn;
|
|
171
|
+
|
|
172
|
+
constructor(
|
|
173
|
+
config: ReclaimProofServiceConfig,
|
|
174
|
+
options: {
|
|
175
|
+
readonly createClient?: (
|
|
176
|
+
applicationId: string,
|
|
177
|
+
applicationSecret: string,
|
|
178
|
+
logs: boolean,
|
|
179
|
+
) => ReclaimClientLike;
|
|
180
|
+
readonly verifyProof?: VerifyProofFn;
|
|
181
|
+
} = {},
|
|
182
|
+
) {
|
|
183
|
+
this.config = {
|
|
184
|
+
...config,
|
|
185
|
+
logs: config.logs ?? false,
|
|
186
|
+
};
|
|
187
|
+
this.client = (options.createClient ?? defaultReclaimClientFactory)(
|
|
188
|
+
this.config.applicationId,
|
|
189
|
+
this.config.applicationSecret,
|
|
190
|
+
this.config.logs ?? false,
|
|
191
|
+
);
|
|
192
|
+
this.verifyProofImpl = options.verifyProof ?? verifyProof;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async proveDeliveryResponse(
|
|
196
|
+
input: ProveDeliveryResponseInput,
|
|
197
|
+
): Promise<VerifiedConsumedHttpsResponseProof> {
|
|
198
|
+
reclaimT2UrlSchema.parse(input.url);
|
|
199
|
+
if (input.responseMatches.length === 0) {
|
|
200
|
+
throw new Error("at least one response match is required");
|
|
201
|
+
}
|
|
202
|
+
const context = deliveryContextEnvelopeV2Schema.parse(input.context);
|
|
203
|
+
const parsed = { ...input, context };
|
|
204
|
+
const method = parsed.method ?? "GET";
|
|
205
|
+
|
|
206
|
+
const proof = await this.client
|
|
207
|
+
.zkFetch(
|
|
208
|
+
parsed.url,
|
|
209
|
+
{
|
|
210
|
+
method,
|
|
211
|
+
...(parsed.body === undefined ? {} : { body: parsed.body }),
|
|
212
|
+
...(parsed.headers === undefined ? {} : { headers: parsed.headers }),
|
|
213
|
+
...(parsed.geoLocation === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { geoLocation: parsed.geoLocation }),
|
|
216
|
+
context: parsed.context,
|
|
217
|
+
...(parsed.useTee === undefined ? {} : { useTee: parsed.useTee }),
|
|
218
|
+
...(parsed.zkEngine === undefined
|
|
219
|
+
? {}
|
|
220
|
+
: { zkEngine: parsed.zkEngine }),
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
...(parsed.secretHeaders === undefined
|
|
224
|
+
? {}
|
|
225
|
+
: { headers: parsed.secretHeaders }),
|
|
226
|
+
responseMatches: parsed.responseMatches,
|
|
227
|
+
...(parsed.responseRedactions === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { responseRedactions: parsed.responseRedactions }),
|
|
230
|
+
...(parsed.cookie === undefined ? {} : { cookieStr: parsed.cookie }),
|
|
231
|
+
...(parsed.secretParamValues === undefined
|
|
232
|
+
? {}
|
|
233
|
+
: { paramValues: parsed.secretParamValues }),
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
.catch((error) => {
|
|
237
|
+
throw new ResponseProofUnavailableError(
|
|
238
|
+
`failed to generate T2 zkTLS delivery proof: ${asErrorMessage(error)}`,
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (proof === undefined) {
|
|
243
|
+
throw new ResponseProofUnavailableError(
|
|
244
|
+
"reclaim returned no zkTLS delivery proof",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const verification = await this.verifyProofImpl(proof, {
|
|
249
|
+
...getProviderHashRequirementsFromSpec({
|
|
250
|
+
requests: [toRequestSpec(parsed, method)],
|
|
251
|
+
}),
|
|
252
|
+
...(parsed.useTee === true
|
|
253
|
+
? { teeAttestation: { appSecret: this.config.applicationSecret } }
|
|
254
|
+
: {}),
|
|
255
|
+
});
|
|
256
|
+
if (!verification.isVerified) {
|
|
257
|
+
throw new ResponseProofVerificationError(
|
|
258
|
+
`T2 zkTLS delivery proof verification failed: ${asErrorMessage(verification.error)}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const trusted = verification.data[0];
|
|
263
|
+
if (trusted === undefined) {
|
|
264
|
+
throw new ResponseProofVerificationError(
|
|
265
|
+
"T2 zkTLS delivery proof verification returned no trusted data",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const request = getHttpProviderClaimParamsFromProof(proof);
|
|
270
|
+
return {
|
|
271
|
+
proof,
|
|
272
|
+
request: {
|
|
273
|
+
url: request.url,
|
|
274
|
+
method: request.method as "GET" | "POST" | "PUT",
|
|
275
|
+
body: request.body === "" ? null : (request.body ?? null),
|
|
276
|
+
responseMatches: request.responseMatches.map((match) => ({
|
|
277
|
+
value: match.value,
|
|
278
|
+
type: match.type,
|
|
279
|
+
invert: match.invert,
|
|
280
|
+
isOptional: match.isOptional,
|
|
281
|
+
})),
|
|
282
|
+
responseRedactions: request.responseRedactions.map((redaction) => ({
|
|
283
|
+
regex: redaction.regex,
|
|
284
|
+
jsonPath: redaction.jsonPath,
|
|
285
|
+
xPath: redaction.xPath,
|
|
286
|
+
hash: redaction.hash,
|
|
287
|
+
})),
|
|
288
|
+
},
|
|
289
|
+
verification: {
|
|
290
|
+
context: trusted.context,
|
|
291
|
+
extractedParameters: trusted.extractedParameters,
|
|
292
|
+
publicData: verification.publicData[0] ?? null,
|
|
293
|
+
isTeeAttestationVerified:
|
|
294
|
+
verification.isTeeAttestationVerified ?? false,
|
|
295
|
+
isAttestorTeeAttestationVerified:
|
|
296
|
+
verification.isAttestorTeeAttestationVerified ?? false,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** `null` when `RECLAIM_APP_ID`/`RECLAIM_APP_SECRET` are not configured (see agent#22). */
|
|
303
|
+
export function createReclaimT2AttestorFromEnv(
|
|
304
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
305
|
+
): ReclaimDeliveryProofAttestor | null {
|
|
306
|
+
const parsed = parseReclaimProofEnv(env);
|
|
307
|
+
if (parsed === null) return null;
|
|
308
|
+
return new ReclaimDeliveryProofAttestor(parsed);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Wire (JSON-safe) form of `DeliveryReceipt` - bigints as decimal strings, hex as `0x`. */
|
|
312
|
+
export interface WireDeliveryReceipt {
|
|
313
|
+
readonly streamId: string;
|
|
314
|
+
readonly serviceRef: `0x${string}`;
|
|
315
|
+
readonly operator: `0x${string}`;
|
|
316
|
+
readonly intervalIndex: string;
|
|
317
|
+
readonly nonce: `0x${string}`;
|
|
318
|
+
readonly reclaimProofId: `0x${string}`;
|
|
319
|
+
readonly reclaimOwner: `0x${string}`;
|
|
320
|
+
readonly reclaimTimestamp: string;
|
|
321
|
+
readonly reclaimEpoch: string;
|
|
322
|
+
readonly canonicalRequestHash: `0x${string}`;
|
|
323
|
+
readonly canonicalBodyHash: `0x${string}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const wireDeliveryReceiptSchema = z
|
|
327
|
+
.object({
|
|
328
|
+
streamId: z.union([
|
|
329
|
+
z.string().regex(bytes32Hex),
|
|
330
|
+
z.string().regex(/^\d+$/),
|
|
331
|
+
]),
|
|
332
|
+
serviceRef: z
|
|
333
|
+
.string()
|
|
334
|
+
.regex(bytes32Hex, "must be a 32-byte hex serviceRef"),
|
|
335
|
+
operator: z.string().regex(evmAddress, "must be an EVM address"),
|
|
336
|
+
intervalIndex: z
|
|
337
|
+
.string()
|
|
338
|
+
.regex(/^\d+$/, "must be a non-negative integer string"),
|
|
339
|
+
nonce: z.string().regex(bytes32Hex, "must be a 32-byte hex nonce"),
|
|
340
|
+
reclaimProofId: z
|
|
341
|
+
.string()
|
|
342
|
+
.regex(bytes32Hex, "must be a 32-byte hex identifier"),
|
|
343
|
+
reclaimOwner: z.string().regex(evmAddress, "must be an EVM address"),
|
|
344
|
+
reclaimTimestamp: z
|
|
345
|
+
.string()
|
|
346
|
+
.regex(/^\d+$/, "must be a non-negative integer string"),
|
|
347
|
+
reclaimEpoch: z
|
|
348
|
+
.string()
|
|
349
|
+
.regex(/^\d+$/, "must be a non-negative integer string"),
|
|
350
|
+
canonicalRequestHash: z
|
|
351
|
+
.string()
|
|
352
|
+
.regex(bytes32Hex, "must be a 32-byte hex hash"),
|
|
353
|
+
canonicalBodyHash: z
|
|
354
|
+
.string()
|
|
355
|
+
.regex(bytes32Hex, "must be a 32-byte hex hash"),
|
|
356
|
+
})
|
|
357
|
+
.strict();
|
|
358
|
+
|
|
359
|
+
const reclaimProviderClaimDataSchema = z
|
|
360
|
+
.object({
|
|
361
|
+
provider: z.string().min(1),
|
|
362
|
+
parameters: z.string().min(1),
|
|
363
|
+
context: z.string(),
|
|
364
|
+
owner: z.string().regex(evmAddress, "must be an EVM address"),
|
|
365
|
+
timestampS: z.number().int().nonnegative(),
|
|
366
|
+
epoch: z.number().int().nonnegative(),
|
|
367
|
+
identifier: z
|
|
368
|
+
.string()
|
|
369
|
+
.regex(bytes32Hex, "must be a 32-byte hex identifier"),
|
|
370
|
+
})
|
|
371
|
+
.strict();
|
|
372
|
+
|
|
373
|
+
const reclaimProofWireSchema = z
|
|
374
|
+
.object({
|
|
375
|
+
identifier: z
|
|
376
|
+
.string()
|
|
377
|
+
.regex(bytes32Hex, "must be a 32-byte hex identifier"),
|
|
378
|
+
claimData: reclaimProviderClaimDataSchema,
|
|
379
|
+
signatures: z.array(z.string().regex(hexSignature)).min(1),
|
|
380
|
+
})
|
|
381
|
+
.strict();
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* `POST /delivery/proof` request body - mirrors the oracle's own
|
|
385
|
+
* `deliveryProofRequestSchema` (`streamproof-oracle/src/delivery/dto.ts`)
|
|
386
|
+
* field-for-field. Kept as a local copy (same convention the oracle itself
|
|
387
|
+
* uses for `@absol-labs/shared` shapes) rather than an import, since the two
|
|
388
|
+
* repos do not share a package for this DTO.
|
|
389
|
+
*/
|
|
390
|
+
export const deliveryProofRequestBodySchema = z
|
|
391
|
+
.object({
|
|
392
|
+
receipt: wireDeliveryReceiptSchema,
|
|
393
|
+
signature: z.string().regex(hexSignature, "must be a hex signature"),
|
|
394
|
+
proof: reclaimProofWireSchema,
|
|
395
|
+
})
|
|
396
|
+
.strict();
|
|
397
|
+
|
|
398
|
+
export type DeliveryProofRequestBody = z.infer<
|
|
399
|
+
typeof deliveryProofRequestBodySchema
|
|
400
|
+
>;
|
|
401
|
+
|
|
402
|
+
/** Converts a bigint-typed `DeliveryReceipt` to the oracle's decimal-string wire shape. */
|
|
403
|
+
export function toWireDeliveryReceipt(
|
|
404
|
+
receipt: DeliveryReceipt,
|
|
405
|
+
): WireDeliveryReceipt {
|
|
406
|
+
return {
|
|
407
|
+
streamId: receipt.streamId.toString(),
|
|
408
|
+
serviceRef: receipt.serviceRef as `0x${string}`,
|
|
409
|
+
operator: receipt.operator as `0x${string}`,
|
|
410
|
+
intervalIndex: receipt.intervalIndex.toString(),
|
|
411
|
+
nonce: receipt.nonce as `0x${string}`,
|
|
412
|
+
reclaimProofId: receipt.reclaimProofId as `0x${string}`,
|
|
413
|
+
reclaimOwner: receipt.reclaimOwner as `0x${string}`,
|
|
414
|
+
reclaimTimestamp: receipt.reclaimTimestamp.toString(),
|
|
415
|
+
reclaimEpoch: receipt.reclaimEpoch.toString(),
|
|
416
|
+
canonicalRequestHash: receipt.canonicalRequestHash as `0x${string}`,
|
|
417
|
+
canonicalBodyHash: receipt.canonicalBodyHash as `0x${string}`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Deterministic keccak256 over the canonical (sorted-key) JSON of `value`. */
|
|
422
|
+
function hashCanonical(value: unknown): `0x${string}` {
|
|
423
|
+
return keccak256(stringToBytes(canonicalJson(value)));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export interface BuildT2DeliveryProofInput {
|
|
427
|
+
/** 32-byte hex, byte-identical to the on-chain `streamId`. */
|
|
428
|
+
readonly streamId: `0x${string}`;
|
|
429
|
+
readonly operator: `0x${string}`;
|
|
430
|
+
readonly serviceRef: `0x${string}`;
|
|
431
|
+
readonly mandateId?: string;
|
|
432
|
+
readonly intervalIndex: bigint;
|
|
433
|
+
/** From `GET /delivery/nonce`. */
|
|
434
|
+
readonly nonce: `0x${string}`;
|
|
435
|
+
/** Unix seconds. Defaults to `Math.floor(Date.now() / 1000)`. */
|
|
436
|
+
readonly issuedAt?: number;
|
|
437
|
+
/** Signs the `DeliveryReceipt`. Never the Reclaim proof's `claimData.owner`. */
|
|
438
|
+
readonly buyer: LocalAccount;
|
|
439
|
+
/** `{chainId, verifyingContract}` - the StreamEscrowV2 the stream settles on. */
|
|
440
|
+
readonly domain: DeliveryReceiptDomainInput;
|
|
441
|
+
readonly request: PaidRouteRequestSpec;
|
|
442
|
+
/** Defaults to `createReclaimT2AttestorFromEnv()`. */
|
|
443
|
+
readonly attestor?: DeliveryProofAttestor;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface BuildT2DeliveryProofResult {
|
|
447
|
+
/** The exact `POST /delivery/proof` request body. */
|
|
448
|
+
readonly body: DeliveryProofRequestBody;
|
|
449
|
+
/** Natural (bigint-typed) receipt, for callers that want it directly. */
|
|
450
|
+
readonly receipt: DeliveryReceipt;
|
|
451
|
+
readonly signature: Hex;
|
|
452
|
+
readonly proof: ReclaimProof;
|
|
453
|
+
readonly envelope: DeliveryContextEnvelopeV2;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export class NoDeliveryAttestorConfiguredError extends Error {
|
|
457
|
+
constructor() {
|
|
458
|
+
super(
|
|
459
|
+
"no zkTLS delivery attestor configured: set RECLAIM_APP_ID/RECLAIM_APP_SECRET " +
|
|
460
|
+
"(see agent#22) or pass an explicit `attestor`",
|
|
461
|
+
);
|
|
462
|
+
this.name = "NoDeliveryAttestorConfiguredError";
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Produces a complete T2 delivery proof envelope: proves the paid route with
|
|
468
|
+
* the oracle-issued nonce bound into both the request and the Reclaim
|
|
469
|
+
* `claimData.context`, signs a `DeliveryReceipt` committing to that proof, and
|
|
470
|
+
* returns the exact body for `POST /delivery/proof`.
|
|
471
|
+
*/
|
|
472
|
+
export async function buildT2DeliveryProofSubmission(
|
|
473
|
+
input: BuildT2DeliveryProofInput,
|
|
474
|
+
): Promise<BuildT2DeliveryProofResult> {
|
|
475
|
+
const issuedAt = input.issuedAt ?? Math.floor(Date.now() / 1000);
|
|
476
|
+
if (input.intervalIndex > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
477
|
+
throw new RangeError("intervalIndex exceeds Number.MAX_SAFE_INTEGER");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const envelope = buildDeliveryContextEnvelopeV2({
|
|
481
|
+
streamId: input.streamId,
|
|
482
|
+
operator: input.operator,
|
|
483
|
+
serviceRef: input.serviceRef,
|
|
484
|
+
...(input.mandateId === undefined ? {} : { mandateId: input.mandateId }),
|
|
485
|
+
nonce: input.nonce,
|
|
486
|
+
intervalIndex: Number(input.intervalIndex),
|
|
487
|
+
issuedAt,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const injected = injectNonce(input.request, input.nonce);
|
|
491
|
+
|
|
492
|
+
const attestor = input.attestor ?? createReclaimT2AttestorFromEnv();
|
|
493
|
+
if (attestor === null) {
|
|
494
|
+
throw new NoDeliveryAttestorConfiguredError();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const proved = await attestor.proveDeliveryResponse({
|
|
498
|
+
url: injected.url,
|
|
499
|
+
method: input.request.method,
|
|
500
|
+
body: injected.body,
|
|
501
|
+
headers: input.request.headers,
|
|
502
|
+
secretHeaders: input.request.secretHeaders,
|
|
503
|
+
responseMatches: input.request.responseMatches,
|
|
504
|
+
responseRedactions: input.request.responseRedactions,
|
|
505
|
+
cookie: input.request.cookie,
|
|
506
|
+
secretParamValues: input.request.secretParamValues,
|
|
507
|
+
geoLocation: input.request.geoLocation,
|
|
508
|
+
useTee: input.request.useTee,
|
|
509
|
+
zkEngine: input.request.zkEngine,
|
|
510
|
+
context: envelope,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const { proof } = proved;
|
|
514
|
+
|
|
515
|
+
// Deterministically bind BOTH the exact proven request (method/url/body, post
|
|
516
|
+
// nonce-injection) and the exact PROVEN (redacted/extracted) response content
|
|
517
|
+
// into the receipt, so the receipt is verifiable dispute evidence even though
|
|
518
|
+
// the oracle itself does not re-derive these hashes today.
|
|
519
|
+
const canonicalRequestHash = hashCanonical({
|
|
520
|
+
method: proved.request.method,
|
|
521
|
+
url: proved.request.url,
|
|
522
|
+
body: proved.request.body,
|
|
523
|
+
});
|
|
524
|
+
const canonicalBodyHash = hashCanonical({
|
|
525
|
+
extractedParameters: proved.verification.extractedParameters,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const receipt = deliveryReceiptSchema.parse({
|
|
529
|
+
streamId: BigInt(input.streamId),
|
|
530
|
+
serviceRef: input.serviceRef,
|
|
531
|
+
operator: input.operator,
|
|
532
|
+
intervalIndex: input.intervalIndex,
|
|
533
|
+
nonce: input.nonce,
|
|
534
|
+
reclaimProofId: proof.identifier as Hex,
|
|
535
|
+
reclaimOwner: proof.claimData.owner as Address,
|
|
536
|
+
reclaimTimestamp: BigInt(proof.claimData.timestampS),
|
|
537
|
+
reclaimEpoch: BigInt(proof.claimData.epoch),
|
|
538
|
+
canonicalRequestHash,
|
|
539
|
+
canonicalBodyHash,
|
|
540
|
+
} satisfies DeliveryReceipt);
|
|
541
|
+
|
|
542
|
+
const signature = await signDeliveryReceipt(
|
|
543
|
+
receipt,
|
|
544
|
+
input.domain,
|
|
545
|
+
input.buyer,
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
const body = deliveryProofRequestBodySchema.parse({
|
|
549
|
+
receipt: toWireDeliveryReceipt(receipt),
|
|
550
|
+
signature,
|
|
551
|
+
proof: {
|
|
552
|
+
identifier: proof.identifier,
|
|
553
|
+
claimData: proof.claimData,
|
|
554
|
+
signatures: proof.signatures,
|
|
555
|
+
},
|
|
556
|
+
} satisfies z.input<typeof deliveryProofRequestBodySchema>);
|
|
557
|
+
|
|
558
|
+
return { body, receipt, signature, proof, envelope };
|
|
559
|
+
}
|