@absol-labs/agent 0.1.0 → 0.3.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 +4 -4
- package/dist/discovery/registry.d.ts +39 -0
- package/dist/discovery/registry.d.ts.map +1 -1
- package/dist/discovery/registry.js +75 -12
- package/dist/discovery/registry.js.map +1 -1
- package/dist/frameworks/agentkit.d.ts +11 -0
- package/dist/frameworks/agentkit.d.ts.map +1 -1
- package/dist/frameworks/agentkit.js +83 -27
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/index.d.ts +2 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/x402/facilitator.d.ts +10 -0
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +59 -10
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/zktls/reclaim.js +1 -1
- package/package.json +18 -1
- package/src/discovery/registry.ts +92 -16
- package/src/frameworks/agentkit.ts +105 -30
- package/src/index.ts +13 -48
- package/src/mcp/server.ts +1 -1
- package/src/x402/facilitator.ts +90 -20
- package/src/zktls/reclaim.ts +1 -1
|
@@ -132,11 +132,6 @@ export interface MetrikMandateStateResolver {
|
|
|
132
132
|
getRevokedMandateIds(owner: `0x${string}`): Promise<readonly `0x${string}`[]>;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const DEFAULT_MANDATE_STATE_RESOLVER: MetrikMandateStateResolver = {
|
|
136
|
-
getSpentSoFarUsdc: async () => 0n,
|
|
137
|
-
getRevokedMandateIds: async () => [],
|
|
138
|
-
};
|
|
139
|
-
|
|
140
135
|
/**
|
|
141
136
|
* The subset of the Metrik SDK the action provider drives. `VerifiedStreamAgentClient`
|
|
142
137
|
* satisfies this structurally; tests inject a lightweight double so the provider
|
|
@@ -205,6 +200,21 @@ export class MetrikActionProviderConfigError extends Error {
|
|
|
205
200
|
}
|
|
206
201
|
}
|
|
207
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Raised when a hire cannot be authorized because a fund-safety mandate control
|
|
205
|
+
* (the cumulative-cap accounting or the revocation state) cannot be POSITIVELY
|
|
206
|
+
* verified — no resolver wired, or a wired resolver threw/returned garbage. The
|
|
207
|
+
* provider denies the hire (fail-closed, buyer-favoring) rather than proceeding
|
|
208
|
+
* on an unverified "0 spent / none revoked" assumption (the B6 fix). Buyer
|
|
209
|
+
* recovery (close / reclaim) is NEVER blocked by this — it is signature-only.
|
|
210
|
+
*/
|
|
211
|
+
export class MetrikSpendControlUnavailableError extends Error {
|
|
212
|
+
constructor(message: string) {
|
|
213
|
+
super(message);
|
|
214
|
+
this.name = "MetrikSpendControlUnavailableError";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
/**
|
|
209
219
|
* Coinbase AgentKit action provider that lets any AgentKit-based agent hire and
|
|
210
220
|
* pay third-party services through Metrik's verified USDC escrow. Every action
|
|
@@ -214,7 +224,10 @@ export class MetrikActionProviderConfigError extends Error {
|
|
|
214
224
|
export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
215
225
|
readonly #client: MetrikAgentKitClient;
|
|
216
226
|
readonly #signedMandate: SignedSpendMandate;
|
|
217
|
-
|
|
227
|
+
// Stored as the raw partial the caller supplied. A missing method means the
|
|
228
|
+
// corresponding fund-safety control cannot be verified, so the HIRE path fails
|
|
229
|
+
// CLOSED (see `#resolveSpendControls`). Buyer recovery never reads this.
|
|
230
|
+
readonly #resolver: Partial<MetrikMandateStateResolver>;
|
|
218
231
|
readonly #now: () => number;
|
|
219
232
|
readonly #chainId: number;
|
|
220
233
|
readonly #discover: (
|
|
@@ -234,23 +247,24 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
|
234
247
|
|
|
235
248
|
this.#client = resolveClient(config);
|
|
236
249
|
this.#signedMandate = config.signedMandate;
|
|
237
|
-
this.#resolver = {
|
|
238
|
-
...DEFAULT_MANDATE_STATE_RESOLVER,
|
|
239
|
-
...config.mandateStateResolver,
|
|
240
|
-
};
|
|
250
|
+
this.#resolver = { ...config.mandateStateResolver };
|
|
241
251
|
// Fail-loud on the two mandate controls that require external state. The
|
|
242
252
|
// action provider holds no stream registry, so without a wired resolver the
|
|
243
|
-
// cumulative cap (maxTotalUsdc) and revocation
|
|
253
|
+
// cumulative cap (maxTotalUsdc) and revocation cannot be verified — and every
|
|
254
|
+
// mandate carries a positive maxTotalUsdc, so a missing spent resolver can
|
|
255
|
+
// NEVER verify the cap. `hire_verified_service` therefore fails CLOSED
|
|
256
|
+
// (denies) until both are wired; warn once so the reason is visible at deploy
|
|
257
|
+
// time. Buyer recovery (close / reclaim) stays available regardless.
|
|
244
258
|
if (config.mandateStateResolver?.getSpentSoFarUsdc === undefined) {
|
|
245
259
|
warnAgentKitOnce(
|
|
246
260
|
"agentkit-spent",
|
|
247
|
-
"[metrik] AgentKit provider: no getSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc)
|
|
261
|
+
"[metrik] AgentKit provider: no getSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc) cannot be verified, so hire_verified_service will DENY every hire (fail-closed) until a resolver is wired.",
|
|
248
262
|
);
|
|
249
263
|
}
|
|
250
264
|
if (config.mandateStateResolver?.getRevokedMandateIds === undefined) {
|
|
251
265
|
warnAgentKitOnce(
|
|
252
266
|
"agentkit-revocation",
|
|
253
|
-
"[metrik] AgentKit provider: no getRevokedMandateIds wired —
|
|
267
|
+
"[metrik] AgentKit provider: no getRevokedMandateIds wired — revocation state cannot be verified, so hire_verified_service will DENY every hire (fail-closed) until a resolver is wired.",
|
|
254
268
|
);
|
|
255
269
|
}
|
|
256
270
|
this.#now = config.now ?? (() => Math.floor(Date.now() / 1000));
|
|
@@ -299,10 +313,10 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
|
299
313
|
|
|
300
314
|
const owner = this.#signedMandate.owner as `0x${string}`;
|
|
301
315
|
const nowSeconds = this.#now();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
316
|
+
// FAIL-CLOSED fund safety (B6): deny the hire unless BOTH the cumulative-cap
|
|
317
|
+
// accounting and the revocation state are positively verified.
|
|
318
|
+
const { spentSoFarUsdc, revokedMandateIds } =
|
|
319
|
+
await this.#resolveSpendControls(owner);
|
|
306
320
|
|
|
307
321
|
const operator = args.operator as `0x${string}`;
|
|
308
322
|
const serviceRef = args.serviceRef as `0x${string}`;
|
|
@@ -386,7 +400,7 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
|
386
400
|
this.#assertWalletNetwork(walletProvider);
|
|
387
401
|
|
|
388
402
|
const result = await this.#client.reclaimStream(
|
|
389
|
-
|
|
403
|
+
this.#authorizedStreamAction(args.streamId as `0x${string}`, {
|
|
390
404
|
...(args.closeFirst === undefined
|
|
391
405
|
? {}
|
|
392
406
|
: { closeFirst: args.closeFirst }),
|
|
@@ -413,7 +427,7 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
|
413
427
|
this.#assertWalletNetwork(walletProvider);
|
|
414
428
|
|
|
415
429
|
const result = await this.#client.closeStream(
|
|
416
|
-
|
|
430
|
+
this.#authorizedStreamAction(args.streamId as `0x${string}`, {}),
|
|
417
431
|
);
|
|
418
432
|
return `Closed stream ${args.streamId}. tx=${result.txHash}`;
|
|
419
433
|
}
|
|
@@ -435,22 +449,79 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
|
435
449
|
}
|
|
436
450
|
}
|
|
437
451
|
|
|
438
|
-
|
|
452
|
+
/**
|
|
453
|
+
* FAIL-CLOSED resolution of the two fund-safety controls for the HIRE path.
|
|
454
|
+
* Every mandate carries a positive `maxTotalUsdc`, so a missing spent resolver
|
|
455
|
+
* can never verify the cap; a missing revocation resolver can never verify
|
|
456
|
+
* revocation. A resolver that throws (state store down) or returns garbage is
|
|
457
|
+
* likewise treated as "cannot verify" → deny. Never a silent fail-open to
|
|
458
|
+
* `{ 0n, [] }`.
|
|
459
|
+
*/
|
|
460
|
+
async #resolveSpendControls(owner: `0x${string}`): Promise<{
|
|
461
|
+
spentSoFarUsdc: bigint;
|
|
462
|
+
revokedMandateIds: readonly `0x${string}`[];
|
|
463
|
+
}> {
|
|
464
|
+
const getSpentSoFarUsdc = this.#resolver.getSpentSoFarUsdc;
|
|
465
|
+
const getRevokedMandateIds = this.#resolver.getRevokedMandateIds;
|
|
466
|
+
if (getSpentSoFarUsdc === undefined) {
|
|
467
|
+
throw new MetrikSpendControlUnavailableError(
|
|
468
|
+
"cannot hire verified service: no getSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc) cannot be verified (fail-closed)",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (getRevokedMandateIds === undefined) {
|
|
472
|
+
throw new MetrikSpendControlUnavailableError(
|
|
473
|
+
"cannot hire verified service: no getRevokedMandateIds wired — mandate revocation state cannot be verified (fail-closed)",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let spentSoFarUsdc: bigint;
|
|
478
|
+
try {
|
|
479
|
+
spentSoFarUsdc = await getSpentSoFarUsdc(owner);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
throw new MetrikSpendControlUnavailableError(
|
|
482
|
+
`cannot verify mandate cumulative cap (maxTotalUsdc): ${asErrorMessage(error)} (fail-closed)`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (typeof spentSoFarUsdc !== "bigint" || spentSoFarUsdc < 0n) {
|
|
486
|
+
throw new MetrikSpendControlUnavailableError(
|
|
487
|
+
"cannot verify mandate cumulative cap (maxTotalUsdc): resolver returned an invalid spent-so-far value (fail-closed)",
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let revokedMandateIds: readonly `0x${string}`[];
|
|
492
|
+
try {
|
|
493
|
+
revokedMandateIds = await getRevokedMandateIds(owner);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
throw new MetrikSpendControlUnavailableError(
|
|
496
|
+
`cannot verify mandate revocation state: ${asErrorMessage(error)} (fail-closed)`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (!Array.isArray(revokedMandateIds)) {
|
|
500
|
+
throw new MetrikSpendControlUnavailableError(
|
|
501
|
+
"cannot verify mandate revocation state: resolver returned a non-array value (fail-closed)",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { spentSoFarUsdc, revokedMandateIds };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Context for a BUYER-RECOVERY action (close / reclaim). These are authorized
|
|
510
|
+
* on the mandate owner's signature ALONE (see `authorizeBuyerRecovery` in the
|
|
511
|
+
* SDK client) and must NEVER be blocked by missing or failing spend-control
|
|
512
|
+
* state — otherwise a lapsed resolver could strand the buyer's own funds. The
|
|
513
|
+
* spent/revoked fields are ignored downstream, so we pass safe neutral values.
|
|
514
|
+
*/
|
|
515
|
+
#authorizedStreamAction<TExtra extends object>(
|
|
439
516
|
streamId: `0x${string}`,
|
|
440
517
|
extra: TExtra,
|
|
441
|
-
):
|
|
442
|
-
const owner = this.#signedMandate.owner as `0x${string}`;
|
|
443
|
-
const nowSeconds = this.#now();
|
|
444
|
-
const [spentSoFarUsdc, revokedMandateIds] = await Promise.all([
|
|
445
|
-
this.#resolver.getSpentSoFarUsdc(owner),
|
|
446
|
-
this.#resolver.getRevokedMandateIds(owner),
|
|
447
|
-
]);
|
|
518
|
+
): MandateAuthorizedStreamActionInput & TExtra {
|
|
448
519
|
return {
|
|
449
520
|
streamId,
|
|
450
521
|
signedMandate: this.#signedMandate,
|
|
451
|
-
spentSoFarUsdc,
|
|
452
|
-
nowSeconds,
|
|
453
|
-
revokedMandateIds,
|
|
522
|
+
spentSoFarUsdc: 0n,
|
|
523
|
+
nowSeconds: this.#now(),
|
|
524
|
+
revokedMandateIds: [],
|
|
454
525
|
...extra,
|
|
455
526
|
};
|
|
456
527
|
}
|
|
@@ -520,6 +591,10 @@ function resolveClient(
|
|
|
520
591
|
);
|
|
521
592
|
}
|
|
522
593
|
|
|
594
|
+
function asErrorMessage(error: unknown): string {
|
|
595
|
+
return error instanceof Error ? error.message : String(error);
|
|
596
|
+
}
|
|
597
|
+
|
|
523
598
|
const USDC_DECIMALS = 6n;
|
|
524
599
|
const USDC_SCALE = 10n ** USDC_DECIMALS;
|
|
525
600
|
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
VerifiedStreamX402Facilitator,
|
|
29
29
|
DeliveryProofUnavailableError,
|
|
30
30
|
X402PayloadError,
|
|
31
|
+
X402SpendControlUnavailableError,
|
|
31
32
|
SettlementTargetMismatchError,
|
|
32
33
|
X402_SCHEME,
|
|
33
34
|
encodeX402PayloadHeader,
|
|
@@ -46,6 +47,8 @@ export {
|
|
|
46
47
|
|
|
47
48
|
export {
|
|
48
49
|
DEFAULT_METRIK_REGISTRY_URL,
|
|
50
|
+
DEMO_SEED_LISTING,
|
|
51
|
+
DEMO_SEED_SERVICE_REF,
|
|
49
52
|
discoverServices,
|
|
50
53
|
type DiscoverServicesOptions,
|
|
51
54
|
type ServiceListing,
|
|
@@ -129,51 +132,13 @@ export {
|
|
|
129
132
|
type VerifyProofFn,
|
|
130
133
|
} from "./zktls/reclaim.js";
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} from "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
METRIK_ELIZA_SETTLEMENT_CHAIN_ID,
|
|
143
|
-
createMetrikElizaPlugin,
|
|
144
|
-
createMetrikElizaContent,
|
|
145
|
-
metrikElizaPlugin,
|
|
146
|
-
// Deprecated aliases retained for backward compatibility.
|
|
147
|
-
createElizaVerifiedStreamContent,
|
|
148
|
-
createElizaVerifiedStreamPlugin,
|
|
149
|
-
type MetrikElizaActionInputs,
|
|
150
|
-
type MetrikElizaActionName,
|
|
151
|
-
type MetrikElizaClient,
|
|
152
|
-
type MetrikElizaMandateStateResolver,
|
|
153
|
-
type MetrikElizaPluginOptions,
|
|
154
|
-
} from "./frameworks/eliza.js";
|
|
155
|
-
|
|
156
|
-
export {
|
|
157
|
-
CREWAI_VERIFIED_STREAM_TOOL_NAMES,
|
|
158
|
-
createCrewAiVerifiedStreamMcpConfig,
|
|
159
|
-
renderCrewAiVerifiedStreamPythonExample,
|
|
160
|
-
type CrewAiVerifiedStreamMcpConfig,
|
|
161
|
-
type CrewAiVerifiedStreamMcpConfigOptions,
|
|
162
|
-
} from "./frameworks/crewai.js";
|
|
163
|
-
|
|
164
|
-
export {
|
|
165
|
-
METRIK_BASE_SEPOLIA_CHAIN_ID,
|
|
166
|
-
MetrikActionProvider,
|
|
167
|
-
MetrikActionProviderConfigError,
|
|
168
|
-
metrikActionProvider,
|
|
169
|
-
createMetrikAgentKit,
|
|
170
|
-
discoverServicesSchema,
|
|
171
|
-
hireVerifiedServiceSchema,
|
|
172
|
-
checkStreamStatusSchema,
|
|
173
|
-
reclaimUnspentSchema,
|
|
174
|
-
closeStreamSchema,
|
|
175
|
-
type CreateMetrikAgentKitOptions,
|
|
176
|
-
type MetrikActionProviderConfig,
|
|
177
|
-
type MetrikAgentKitClient,
|
|
178
|
-
type MetrikMandateStateResolver,
|
|
179
|
-
} from "./frameworks/agentkit.js";
|
|
135
|
+
// Framework adapters are NOT re-exported from this top-level barrel on purpose.
|
|
136
|
+
// Some of them (notably the Coinbase AgentKit adapter) pull heavy optional
|
|
137
|
+
// dependency graphs, so importing "@absol-labs/agent" must stay light and load
|
|
138
|
+
// only the core (SDK client, spend mandates, MCP server, x402, discovery,
|
|
139
|
+
// wallet, zkTLS). Import an adapter from its own subpath when you need it:
|
|
140
|
+
//
|
|
141
|
+
// import { createMetrikAgentKit } from "@absol-labs/agent/agentkit";
|
|
142
|
+
// import { metrikElizaPlugin } from "@absol-labs/agent/eliza";
|
|
143
|
+
// import { createLangChainVerifiedStreamTools } from "@absol-labs/agent/langchain";
|
|
144
|
+
// import { createCrewAiVerifiedStreamMcpConfig } from "@absol-labs/agent/crewai";
|
package/src/mcp/server.ts
CHANGED
|
@@ -188,7 +188,7 @@ export const METRIK_MCP_TOOLS: readonly McpToolSpec[] = [
|
|
|
188
188
|
{
|
|
189
189
|
name: "prove_https_response",
|
|
190
190
|
description:
|
|
191
|
-
"Generate a zkTLS proof of the exact HTTPS response consumed for a tracked stream. This is an
|
|
191
|
+
"Generate a zkTLS proof of the exact HTTPS response consumed for a tracked stream. This is an L2 delivery signal (consumer zkTLS) and must be cross-checked with oracle/L1 evidence.",
|
|
192
192
|
movesFunds: false,
|
|
193
193
|
},
|
|
194
194
|
] as const;
|
package/src/x402/facilitator.ts
CHANGED
|
@@ -130,17 +130,37 @@ export class DeliveryProofUnavailableError extends Error {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Raised when a spend cannot be authorized because a fund-safety mandate control
|
|
135
|
+
* (the cumulative-cap accounting or the revocation state) cannot be POSITIVELY
|
|
136
|
+
* verified — no resolver wired, or a wired resolver threw/returned garbage. The
|
|
137
|
+
* facilitator denies the open in this case (fail-closed, buyer-favoring) rather
|
|
138
|
+
* than proceeding on an unverified assumption of "0 spent / none revoked".
|
|
139
|
+
*/
|
|
140
|
+
export class X402SpendControlUnavailableError extends Error {
|
|
141
|
+
constructor(message: string) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "X402SpendControlUnavailableError";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
134
148
|
private readonly agentClient: VerifiedStreamAgentOpener;
|
|
135
149
|
private readonly deliveryProofs: ConsumerDeliveryProofService | undefined;
|
|
136
150
|
private readonly sdkConfig: StreamProofClientConfig;
|
|
137
151
|
private readonly nowSeconds: () => number;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
// Left `undefined` when the caller wired no resolver. A missing resolver means
|
|
153
|
+
// the corresponding fund-safety control cannot be positively verified, so
|
|
154
|
+
// `open()` fails CLOSED rather than defaulting to a fail-open "0 spent / none
|
|
155
|
+
// revoked" assumption (the B6 fix).
|
|
156
|
+
private readonly resolveSpentSoFarUsdc:
|
|
157
|
+
| ((signedMandate: SignedSpendMandate) => bigint | Promise<bigint>)
|
|
158
|
+
| undefined;
|
|
159
|
+
private readonly resolveRevokedMandateIds:
|
|
160
|
+
| ((
|
|
161
|
+
signedMandate: SignedSpendMandate,
|
|
162
|
+
) => Iterable<`0x${string}`> | Promise<Iterable<`0x${string}`>>)
|
|
163
|
+
| undefined;
|
|
144
164
|
|
|
145
165
|
constructor(options: VerifiedStreamFacilitatorOptions) {
|
|
146
166
|
this.sdkConfig = options.sdkConfig;
|
|
@@ -150,24 +170,25 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
150
170
|
this.deliveryProofs = options.deliveryProofs;
|
|
151
171
|
this.nowSeconds =
|
|
152
172
|
options.nowSeconds ?? (() => Math.floor(Date.now() / 1000));
|
|
153
|
-
this.resolveSpentSoFarUsdc = options.resolveSpentSoFarUsdc
|
|
154
|
-
this.resolveRevokedMandateIds =
|
|
155
|
-
options.resolveRevokedMandateIds ?? (() => []);
|
|
173
|
+
this.resolveSpentSoFarUsdc = options.resolveSpentSoFarUsdc;
|
|
174
|
+
this.resolveRevokedMandateIds = options.resolveRevokedMandateIds;
|
|
156
175
|
|
|
157
176
|
// Fail-loud on the two mandate controls that require external state. This
|
|
158
|
-
// facilitator is stateless (no stream registry), so
|
|
159
|
-
//
|
|
160
|
-
//
|
|
177
|
+
// facilitator is stateless (no stream registry), so without a wired resolver
|
|
178
|
+
// it cannot verify the cumulative cap (maxTotalUsdc) or revocation — and
|
|
179
|
+
// every mandate carries a positive maxTotalUsdc, so a missing spent resolver
|
|
180
|
+
// can NEVER verify the cap. `open()` therefore fails CLOSED (denies) until
|
|
181
|
+
// both resolvers are wired; warn once so the reason is visible at deploy time.
|
|
161
182
|
if (options.resolveSpentSoFarUsdc === undefined) {
|
|
162
183
|
warnFacilitatorOnce(
|
|
163
184
|
"x402-spent",
|
|
164
|
-
"[metrik] x402 facilitator: no resolveSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc)
|
|
185
|
+
"[metrik] x402 facilitator: no resolveSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc) cannot be verified, so open() will DENY every spend (fail-closed) until a resolver is wired.",
|
|
165
186
|
);
|
|
166
187
|
}
|
|
167
188
|
if (options.resolveRevokedMandateIds === undefined) {
|
|
168
189
|
warnFacilitatorOnce(
|
|
169
190
|
"x402-revocation",
|
|
170
|
-
"[metrik] x402 facilitator: no resolveRevokedMandateIds wired —
|
|
191
|
+
"[metrik] x402 facilitator: no resolveRevokedMandateIds wired — revocation state cannot be verified, so open() will DENY every spend (fail-closed) until a resolver is wired.",
|
|
171
192
|
);
|
|
172
193
|
}
|
|
173
194
|
}
|
|
@@ -182,12 +203,61 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
182
203
|
const payload = parseX402PayloadHeader(payloadHeader);
|
|
183
204
|
assertSettlementTarget(payload.challenge.requirements, this.sdkConfig);
|
|
184
205
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
206
|
+
// FAIL-CLOSED fund safety (B6). Both the cumulative-cap accounting and the
|
|
207
|
+
// revocation state must be POSITIVELY verified before any spend. A missing
|
|
208
|
+
// resolver, a resolver that throws (state store down), or a resolver that
|
|
209
|
+
// returns a non-bigint/negative spend is treated as "state cannot be
|
|
210
|
+
// verified" and DENIES the open — never a silent fail-open to 0/none.
|
|
211
|
+
if (this.resolveSpentSoFarUsdc === undefined) {
|
|
212
|
+
throw new X402SpendControlUnavailableError(
|
|
213
|
+
"cannot open verified stream: no resolveSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc) cannot be verified (fail-closed)",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (this.resolveRevokedMandateIds === undefined) {
|
|
217
|
+
throw new X402SpendControlUnavailableError(
|
|
218
|
+
"cannot open verified stream: no resolveRevokedMandateIds wired — mandate revocation state cannot be verified (fail-closed)",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let spentSoFarUsdc: bigint;
|
|
223
|
+
try {
|
|
224
|
+
spentSoFarUsdc = await this.resolveSpentSoFarUsdc(payload.signedMandate);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new X402SpendControlUnavailableError(
|
|
227
|
+
`cannot verify mandate cumulative cap (maxTotalUsdc): ${
|
|
228
|
+
error instanceof Error ? error.message : String(error)
|
|
229
|
+
} (fail-closed)`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (typeof spentSoFarUsdc !== "bigint" || spentSoFarUsdc < 0n) {
|
|
233
|
+
throw new X402SpendControlUnavailableError(
|
|
234
|
+
"cannot verify mandate cumulative cap (maxTotalUsdc): resolver returned an invalid spent-so-far value (fail-closed)",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let revokedMandateIds: Iterable<`0x${string}`>;
|
|
239
|
+
try {
|
|
240
|
+
revokedMandateIds = await this.resolveRevokedMandateIds(
|
|
241
|
+
payload.signedMandate,
|
|
242
|
+
);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw new X402SpendControlUnavailableError(
|
|
245
|
+
`cannot verify mandate revocation state: ${
|
|
246
|
+
error instanceof Error ? error.message : String(error)
|
|
247
|
+
} (fail-closed)`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (
|
|
251
|
+
revokedMandateIds === null ||
|
|
252
|
+
revokedMandateIds === undefined ||
|
|
253
|
+
typeof (revokedMandateIds as { [Symbol.iterator]?: unknown })[
|
|
254
|
+
Symbol.iterator
|
|
255
|
+
] !== "function"
|
|
256
|
+
) {
|
|
257
|
+
throw new X402SpendControlUnavailableError(
|
|
258
|
+
"cannot verify mandate revocation state: resolver returned a non-iterable value (fail-closed)",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
191
261
|
|
|
192
262
|
const result = await this.agentClient.openVerifiedStream({
|
|
193
263
|
operator: payload.challenge.requirements.operator as `0x${string}`,
|
package/src/zktls/reclaim.ts
CHANGED
|
@@ -346,7 +346,7 @@ function buildRequestContext(
|
|
|
346
346
|
return {
|
|
347
347
|
contextAddress: binding.owner,
|
|
348
348
|
contextMessage: JSON.stringify({
|
|
349
|
-
kind: "metrik-
|
|
349
|
+
kind: "metrik-l2-http-response/v1",
|
|
350
350
|
streamId: binding.streamId ?? null,
|
|
351
351
|
operator: binding.operator ?? null,
|
|
352
352
|
serviceRef: binding.serviceRef ?? null,
|