@agenticprimitives/delegation 1.0.0-alpha.12 → 1.0.0-alpha.14

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/dist/quorum.js ADDED
@@ -0,0 +1,80 @@
1
+ // Off-chain quorum verification (spec 291 §6b). For MCP-only operations that never
2
+ // redeem on chain, verify a QuorumCaveat off-chain: threshold satisfied, every
3
+ // signer authorized (in the caveat whitelist), every signature valid over the
4
+ // bound payload — via the connection-agnostic signature verifier (EOA ECDSA /
5
+ // ERC-1271 / ERC-6492). Fail-closed: any decode error, unbound payload, or
6
+ // sub-threshold match rejects.
7
+ //
8
+ // Binding: the quorum signs over `expectedPayloadHash` (the delegation's EIP-712
9
+ // hash) — i.e. the quorum approves THIS exact delegation. A proof minted for one
10
+ // delegation cannot be replayed onto another (each has a unique hash).
11
+ //
12
+ // Port: smart-agent RecoveryEnforcer.sol (guardian threshold + per-signer +
13
+ // intent-bound). Divergence: that verifies on-chain at redeem; this is the
14
+ // off-chain equivalent for tools that never redeem.
15
+ import { decodeAbiParameters } from 'viem';
16
+ /** Split a 0x-prefixed blob of concatenated 65-byte signatures; null if malformed. */
17
+ function splitPackedSignatures(packed) {
18
+ const hex = packed.startsWith('0x') ? packed.slice(2) : packed;
19
+ if (hex.length === 0 || hex.length % 130 !== 0)
20
+ return null; // 65 bytes = 130 hex chars
21
+ const out = [];
22
+ for (let i = 0; i < hex.length; i += 130)
23
+ out.push(`0x${hex.slice(i, i + 130)}`);
24
+ return out;
25
+ }
26
+ export async function verifyOffChainQuorum(input) {
27
+ // 1. Payload binding — the quorum must have signed THIS delegation's hash.
28
+ if (input.proof.payloadHash.toLowerCase() !== input.expectedPayloadHash.toLowerCase()) {
29
+ return { ok: false, reason: 'payload_unbound' };
30
+ }
31
+ // 2. Decode the caveat's authorized signer set + threshold.
32
+ let signers;
33
+ let threshold;
34
+ try {
35
+ const [s, t] = decodeAbiParameters([{ type: 'address[]' }, { type: 'uint8' }, { type: 'address' }], input.caveatTerms);
36
+ signers = s.map((a) => a);
37
+ threshold = Number(t);
38
+ }
39
+ catch {
40
+ return { ok: false, reason: 'bad_caveat_terms' };
41
+ }
42
+ if (!Number.isFinite(threshold) || threshold <= 0)
43
+ return { ok: false, reason: 'bad_threshold' };
44
+ if (signers.length < threshold)
45
+ return { ok: false, reason: 'whitelist_smaller_than_threshold' };
46
+ // 3. Split the packed signatures.
47
+ const sigs = splitPackedSignatures(input.proof.packedSignatures);
48
+ if (sigs === null)
49
+ return { ok: false, reason: 'bad_signatures' };
50
+ if (sigs.length < threshold)
51
+ return { ok: false, reason: 'insufficient_signatures' };
52
+ // 4. Match each signature to a UNIQUE authorized signer (no double-count).
53
+ const remaining = new Set(signers.map((a) => a.toLowerCase()));
54
+ const matched = [];
55
+ for (const sig of sigs) {
56
+ for (const signer of signers) {
57
+ if (!remaining.has(signer.toLowerCase()))
58
+ continue;
59
+ let ok = false;
60
+ try {
61
+ ok = await input.verifySignature(signer, input.proof.payloadHash, sig);
62
+ }
63
+ catch {
64
+ ok = false; // a throwing verifier is a failed check (fail-closed)
65
+ }
66
+ if (ok) {
67
+ remaining.delete(signer.toLowerCase());
68
+ matched.push(signer);
69
+ break;
70
+ }
71
+ }
72
+ if (matched.length >= threshold)
73
+ break;
74
+ }
75
+ if (matched.length < threshold) {
76
+ return { ok: false, reason: `threshold_unmet (${matched.length}/${threshold})` };
77
+ }
78
+ return { ok: true, matched };
79
+ }
80
+ //# sourceMappingURL=quorum.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quorum.js","sourceRoot":"","sources":["../src/quorum.ts"],"names":[],"mappings":"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"}
package/dist/token.d.ts CHANGED
@@ -67,6 +67,68 @@ export interface VerifyOptsExt extends VerifyOpts {
67
67
  * `process.env.NODE_ENV` is undefined).
68
68
  */
69
69
  revocationFailMode?: 'closed' | 'open';
70
+ /**
71
+ * spec 289 §5 — optional injected revocation reader. When provided, the
72
+ * revocation check routes through it (resilient, multi-provider, evidence-
73
+ * bearing, with the bounded-freshness invariant) INSTEAD of the inline
74
+ * single-client `isRevoked` read. Structurally compatible with chain-state's
75
+ * `ChainAuthorityReader` (so delegation needs no chain-state dependency). A
76
+ * fail-closed reader result (evidence.source begins `fail-closed`) is treated
77
+ * exactly like the inline read's RPC-throw: rejected under `revocationFailMode:
78
+ * 'closed'` (default), tolerated under `'open'`. Omit ⇒ unchanged inline read.
79
+ */
80
+ chainRevocationReader?: {
81
+ isDelegationRevoked(hash: Hex): Promise<{
82
+ revoked: boolean;
83
+ evidence?: {
84
+ source: string;
85
+ };
86
+ }>;
87
+ };
88
+ /**
89
+ * spec 289 §5 (W2) — optional injected acceptance reader. When provided AND `requireAcceptedOnChain`,
90
+ * the `hasAcceptedSessionDelegation` high-risk gate routes through it (resilient, multi-provider,
91
+ * evidence-bearing) INSTEAD of the inline single-client read. Structurally compatible with chain-state's
92
+ * `ChainAuthorityReader` (so delegation needs no chain-state dependency). Acceptance is inherently
93
+ * fail-closed (the tier REQUIRES `accepted === true`): a fail-closed reader result (evidence.source
94
+ * begins `fail-closed`) or a throw is rejected, exactly like the inline read's RPC-throw. Omit ⇒
95
+ * unchanged inline read.
96
+ */
97
+ chainAcceptanceReader?: {
98
+ isSessionDelegationAccepted(principal: Address, hash: Hex): Promise<{
99
+ accepted: boolean;
100
+ evidence?: {
101
+ source: string;
102
+ };
103
+ }>;
104
+ };
105
+ /**
106
+ * spec 289 §5 (W3) — optional injected signature reader. When provided, BOTH signature-verify sites
107
+ * (the main delegator signature AND the DEL-001 session-delegation leaf) route through it (resilient,
108
+ * multi-provider, evidence-bearing) INSTEAD of the inline single-client USV/ERC-1271 read. Structurally
109
+ * compatible with chain-state's `ChainAuthorityReader.verifySmartAgentSignature` (so delegation needs no
110
+ * chain-state dependency). The reader validates via the UniversalSignatureValidator (connection-agnostic:
111
+ * ECDSA / ERC-1271 / ERC-6492) and reports `deployed` as a FACT; this verifier keeps the POLICY:
112
+ * - a fail-closed reader result (evidence.source begins `fail-closed`) or a throw → reject (signature
113
+ * is critical — fail closed, like the inline read's revert);
114
+ * - `valid === false` → reject;
115
+ * - `requireDeployed && !deployed` → reject (a counterfactual/undeployed SA; the caller must opt into
116
+ * it explicitly with `requireDeployed: false`, exactly as the legacy inline path required).
117
+ * Omit ⇒ unchanged inline read (USV when `universalSignatureValidator` is set, else legacy ERC-1271).
118
+ */
119
+ chainSignatureReader?: {
120
+ verifySmartAgentSignature(input: {
121
+ signer: Address;
122
+ digest: Hex;
123
+ signature: Hex;
124
+ }): Promise<{
125
+ valid: boolean;
126
+ deployed: boolean;
127
+ evidence?: {
128
+ source: string;
129
+ };
130
+ }>;
131
+ };
70
132
  /**
71
133
  * Audit sink (audit C3 pass 3b). When provided, every verify outcome
72
134
  * emits a `delegation.verify.{accept,reject}` event. The principal
@@ -133,6 +195,17 @@ export interface VerifyOptsExt extends VerifyOpts {
133
195
  payloadHash: Hex;
134
196
  packedSignatures: Hex;
135
197
  };
198
+ /**
199
+ * Usage-accounting mode for the delegation token's `usageLimit`/JTI (spec 290 §3).
200
+ * - `'consume'` (DEFAULT — unchanged): atomically consume one use at verify.
201
+ * - `'reserve'`: reserve an in-flight use at verify and return a
202
+ * `JtiReservation` on the success result; the caller `commit()`s ONLY after
203
+ * the call is admitted + executed, and `release()`s on any later rejection
204
+ * (rate limit, budget, policy) so a post-verify failure never burns a use.
205
+ * Requires the `jtiStore` to implement `reserveUsage` (else verify fails
206
+ * closed — no silent fallback to consume).
207
+ */
208
+ usageMode?: 'consume' | 'reserve';
136
209
  /**
137
210
  * When `true`, verify that the delegator's smart account has
138
211
  * pre-blessed this delegation hash on-chain via
@@ -165,6 +238,8 @@ export declare function sessionDelegateBindingError(delegation: Delegation, sess
165
238
  */
166
239
  export declare function verifyDelegationToken(token: string, opts: VerifyOptsExt): Promise<{
167
240
  principal: Address;
241
+ sessionKeyAddress: Address;
168
242
  grants?: DataScopeGrant[];
243
+ usage?: import('./types.js').JtiReservation;
169
244
  } | VerifyError>;
170
245
  //# sourceMappingURL=token.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAeA,OAAO,EAA4B,KAAK,OAAO,EAAE,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AACxE,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EACrB,WAAW,EACX,UAAU,EACV,cAAc,EACf,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAkDtE;;;;;;;gBAOgB;AAChB,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAsBpD;AAiDD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAClG,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,EAC1C,IAAI,CAAC,EAAE;IACL,iEAAiE;IACjE,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA8EzC;AA2DD,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACvC;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IASvB;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,WAAW,CAAC,EACR;QAAE,IAAI,EAAE,mBAAmB,CAAA;KAAE,GAC7B;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,WAAW,EAAE,GAAG,CAAC;QAAC,gBAAgB,EAAE,GAAG,CAAA;KAAE,CAAC;IAEnE;;;;;;;;;;;OAWG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AA+BD;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,UAAU,GAAG,SAAS,EACzC,iBAAiB,EAAE,OAAO,GACzB,MAAM,GAAG,IAAI,CASf;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;CAAE,GAAG,WAAW,CAAC,CAmU1E"}
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"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"}
package/dist/token.js CHANGED
@@ -15,6 +15,9 @@ import { keccak_256 } from '@noble/hashes/sha3.js';
15
15
  import { createPublicClient, http } from 'viem';
16
16
  import { hashDelegation } from './hash.js';
17
17
  import { evaluateCaveats } from './evaluator.js';
18
+ import { DATA_SCOPE_ENFORCER, decodeDataScopeTerms } from './caveats.js';
19
+ import { verifyUniversalSignature } from './onchain.js';
20
+ import { verifyOffChainQuorum } from './quorum.js';
18
21
  import { buildEvent } from '@agenticprimitives/audit';
19
22
  // ─── base64url + canonical JSON helpers ──────────────────────────────────
20
23
  //
@@ -255,7 +258,7 @@ const UNIVERSAL_SIG_VALIDATOR_ABI = [
255
258
  const ACCEPTED_SESSION_DELEGATION_ABI = [
256
259
  {
257
260
  type: 'function',
258
- name: 'isAcceptedSessionDelegation',
261
+ name: 'hasAcceptedSessionDelegation',
259
262
  stateMutability: 'view',
260
263
  inputs: [{ name: 'sessionDelegationHash', type: 'bytes32' }],
261
264
  outputs: [{ name: '', type: 'bool' }],
@@ -380,24 +383,100 @@ export async function verifyDelegationToken(token, opts) {
380
383
  args: [signer, hash, sig],
381
384
  }))
382
385
  : null;
383
- try {
384
- const revoked = (await publicClient.readContract({
385
- address: opts.delegationManager,
386
- abi: DELEGATION_MANAGER_ABI,
387
- functionName: 'isRevoked',
388
- args: [eip712Hash],
389
- }));
390
- if (revoked) {
391
- return rejectWith('delegation revoked', claims.delegation.delegator, eip712Hash);
386
+ // spec 289 §5 (W3) — validate an SA signature through the resilient, evidence-bearing port when a
387
+ // `chainSignatureReader` is injected. The port surfaces FACTS ({valid, deployed}); this closure keeps
388
+ // the POLICY: fail-closed/throw → reject (signature is critical); !valid → reject; requireDeployed &&
389
+ // !deployed → reject (counterfactual needs explicit requireDeployed=false). Returns null on success,
390
+ // else a rejection reason. Used by BOTH signature sites below.
391
+ const validateSaSigViaReader = async (signer, hash, sig) => {
392
+ let valid = false;
393
+ let deployed = false;
394
+ let failClosed = false;
395
+ try {
396
+ const r = await opts.chainSignatureReader.verifySmartAgentSignature({ signer, digest: hash, signature: sig });
397
+ valid = r.valid;
398
+ deployed = r.deployed;
399
+ failClosed = r.evidence?.source?.startsWith('fail-closed') ?? false;
392
400
  }
393
- }
394
- catch (e) {
395
- if (revocationFailMode === 'closed') {
396
- return rejectWith('revocation check unavailable; refusing to verify (set revocationFailMode=open only for explicit dev/demo paths)', claims.delegation.delegator, eip712Hash);
401
+ catch {
402
+ // A throwing reader is itself an unconfirmable signature check.
403
+ failClosed = true;
404
+ }
405
+ if (failClosed)
406
+ return 'signature check unavailable; refusing to verify';
407
+ if (!valid)
408
+ return 'signature validation failed';
409
+ if (requireDeployed && !deployed) {
410
+ return 'delegator smart account is not deployed — cannot honor requireDeployed. Set verifyDelegationToken opt requireDeployed=false only for explicit counterfactual-demo use cases.';
411
+ }
412
+ return null;
413
+ };
414
+ // DEL-002 (seam audit): the SAME fail-closed revocation check, applied to a given delegation hash so it
415
+ // can gate BOTH the root delegation AND the DEL-001 session-delegation LEAF. Returns a rejection reason
416
+ // or null. spec 289 §5: the resilient reader is fail-closed by construction (an unconfirmable read comes
417
+ // back revoked:true with evidence.source='fail-closed:*'); map that to the inline read's RPC-throw —
418
+ // reject under 'closed', tolerate under 'open'.
419
+ const revocationRejection = async (hash) => {
420
+ if (opts.chainRevocationReader) {
421
+ let revoked = true;
422
+ let failClosed = false;
423
+ try {
424
+ const r = await opts.chainRevocationReader.isDelegationRevoked(hash);
425
+ revoked = r.revoked;
426
+ failClosed = r.evidence?.source?.startsWith('fail-closed') ?? false;
427
+ }
428
+ catch {
429
+ revoked = true;
430
+ failClosed = true;
431
+ }
432
+ if (revoked) {
433
+ if (failClosed && revocationFailMode === 'open')
434
+ return null; // dev/demo tolerates an unconfirmable read
435
+ return failClosed
436
+ ? 'revocation check unavailable; refusing to verify (set revocationFailMode=open only for explicit dev/demo paths)'
437
+ : 'delegation revoked';
438
+ }
439
+ return null;
440
+ }
441
+ try {
442
+ const revoked = (await publicClient.readContract({
443
+ address: opts.delegationManager,
444
+ abi: DELEGATION_MANAGER_ABI,
445
+ functionName: 'isRevoked',
446
+ args: [hash],
447
+ }));
448
+ return revoked ? 'delegation revoked' : null;
397
449
  }
398
- void e;
450
+ catch (e) {
451
+ void e;
452
+ return revocationFailMode === 'closed'
453
+ ? 'revocation check unavailable; refusing to verify (set revocationFailMode=open only for explicit dev/demo paths)'
454
+ : null;
455
+ }
456
+ };
457
+ {
458
+ // Root delegation revocation (unchanged semantics).
459
+ const rootReason = await revocationRejection(eip712Hash);
460
+ if (rootReason)
461
+ return rejectWith(rootReason, claims.delegation.delegator, eip712Hash);
462
+ // DEL-002: the session-delegation LEAF (DEL-001 session-key binding) is separately revocable on-chain
463
+ // (revokeDelegation(leafHash)), but was NEVER checked — so revoking the session-key binding did not
464
+ // invalidate the token until the ROOT was also revoked. Check the leaf with the SAME fail-closed model
465
+ // so a revoked (e.g. compromised) session key is rejected as tightly as a revoked root.
466
+ if (claims.sessionDelegation) {
467
+ const leafHash = hashDelegation(claims.sessionDelegation, opts.chainId, opts.delegationManager);
468
+ const leafReason = await revocationRejection(leafHash);
469
+ if (leafReason)
470
+ return rejectWith(`session-delegation leaf: ${leafReason}`, claims.delegation.delegator, leafHash);
471
+ }
472
+ }
473
+ if (opts.chainSignatureReader) {
474
+ // spec 289 §5 (W3) — main delegator signature through the resilient port.
475
+ const reason = await validateSaSigViaReader(claims.delegation.delegator, eip712Hash, claims.delegation.signature);
476
+ if (reason)
477
+ return rejectWith(reason, claims.delegation.delegator, eip712Hash);
399
478
  }
400
- if (validateSaSig) {
479
+ else if (validateSaSig) {
401
480
  // spec 270 v4 — one connection-agnostic surface; counterfactual delegators verify via ERC-6492, so
402
481
  // no requireDeployed hard-reject.
403
482
  try {
@@ -457,7 +536,13 @@ export async function verifyDelegationToken(token, opts) {
457
536
  // strategy passes through one surface), else legacy deployed-ERC-1271.
458
537
  const sdHash = hashDelegation(sd, opts.chainId, opts.delegationManager);
459
538
  try {
460
- if (validateSaSig) {
539
+ if (opts.chainSignatureReader) {
540
+ // spec 289 §5 (W3) — DEL-001 leaf signature through the resilient port.
541
+ const reason = await validateSaSigViaReader(sd.delegator, sdHash, sd.signature);
542
+ if (reason)
543
+ return rejectWith(`session-delegation ${reason} (DEL-001)`, claims.delegation.delegator, eip712Hash);
544
+ }
545
+ else if (validateSaSig) {
461
546
  if (!(await validateSaSig(sd.delegator, sdHash, sd.signature))) {
462
547
  return rejectWith('session-delegation signature validation failed (DEL-001)', claims.delegation.delegator, eip712Hash);
463
548
  }
@@ -520,42 +605,99 @@ export async function verifyDelegationToken(token, opts) {
520
605
  return rejectWith('quorum caveat present but no quorumProof supplied — refusing to assume on-chain enforcement', claims.delegation.delegator, eip712Hash);
521
606
  }
522
607
  if (opts.quorumProof.mode === 'off-chain') {
523
- // Off-chain verification is a planned mode; the wire-format
524
- // (payloadHash + packedSignatures) is reserved here so consumers
525
- // can migrate when the implementation lands. Failing closed
526
- // until the spec'd off-chain payload format is implemented.
527
- return rejectWith('quorum_off_chain_not_implemented', claims.delegation.delegator, eip712Hash);
608
+ // Spec 291 §6b — verify the QuorumCaveat off-chain (threshold + authorized
609
+ // signers + signatures bound to THIS delegation's hash). For tools that
610
+ // never redeem on chain. Requires the UniversalSignatureValidator surface
611
+ // so any signer credential strategy (EOA / ERC-1271 / ERC-6492) works.
612
+ if (!opts.universalSignatureValidator) {
613
+ return rejectWith('quorum_off_chain_requires_universalSignatureValidator', claims.delegation.delegator, eip712Hash);
614
+ }
615
+ const usv = opts.universalSignatureValidator;
616
+ const quorumCaveat = claims.delegation.caveats.find((c) => c.enforcer.toLowerCase() === target);
617
+ if (!quorumCaveat) {
618
+ // Defensive — hasQuorum above already guaranteed presence.
619
+ return rejectWith('quorum caveat vanished', claims.delegation.delegator, eip712Hash);
620
+ }
621
+ const quorumResult = await verifyOffChainQuorum({
622
+ caveatTerms: quorumCaveat.terms,
623
+ proof: opts.quorumProof,
624
+ // Bind the quorum to THIS delegation (its EIP-712 hash) — replay onto a
625
+ // different delegation fails the payload-binding check.
626
+ expectedPayloadHash: eip712Hash,
627
+ verifySignature: (signer, digest, signature) => verifyUniversalSignature({ signer, digest, signature, rpcUrl: opts.rpcUrl, universalSignatureValidator: usv }),
628
+ });
629
+ if (!quorumResult.ok) {
630
+ return rejectWith(`quorum_off_chain: ${quorumResult.reason}`, claims.delegation.delegator, eip712Hash);
631
+ }
632
+ // Verified — fall through to the rest of the pipeline.
528
633
  }
529
634
  // mode === 'on-chain-redeemed' — caller asserts the call will
530
635
  // pass through QuorumEnforcer.beforeHook on chain. Accept; the
531
636
  // on-chain layer is responsible for the actual signature check.
532
637
  }
533
- // - requireAcceptedOnChain: account.isAcceptedSessionDelegation(hash)
638
+ // - requireAcceptedOnChain: account.hasAcceptedSessionDelegation(hash)
534
639
  // must return true. Spec § 6 high-risk gate. Single extra chain
535
640
  // read; only fires when the tool-policy decision flips this on
536
641
  // (typically critical-tier tools).
537
642
  let acceptedOnChain = false;
538
643
  if (opts.requireAcceptedOnChain) {
539
- try {
540
- acceptedOnChain = (await publicClient.readContract({
541
- address: claims.delegation.delegator,
542
- abi: ACCEPTED_SESSION_DELEGATION_ABI,
543
- functionName: 'isAcceptedSessionDelegation',
544
- args: [eip712Hash],
545
- }));
546
- }
547
- catch (e) {
548
- return rejectWith(`acceptSessionDelegation check reverted: ${e instanceof Error ? e.message : e}`, claims.delegation.delegator, eip712Hash);
644
+ if (opts.chainAcceptanceReader) {
645
+ // spec 289 §5 (W2) — acceptance through the resilient, evidence-bearing port. Inherently
646
+ // fail-closed: not-accepted OR an unconfirmable read (fail-closed evidence / throw) both reject,
647
+ // mapping the inline read's RPC-throw → reject (this gate has no 'open' mode — the tier requires it).
648
+ let failClosed = false;
649
+ try {
650
+ const r = await opts.chainAcceptanceReader.isSessionDelegationAccepted(claims.delegation.delegator, eip712Hash);
651
+ acceptedOnChain = r.accepted;
652
+ failClosed = r.evidence?.source?.startsWith('fail-closed') ?? false;
653
+ }
654
+ catch {
655
+ acceptedOnChain = false;
656
+ failClosed = true;
657
+ }
658
+ if (!acceptedOnChain) {
659
+ return rejectWith(failClosed
660
+ ? 'acceptSessionDelegation check unavailable; refusing to verify'
661
+ : 'tier requires on-chain acceptSessionDelegation blessing; not present', claims.delegation.delegator, eip712Hash);
662
+ }
549
663
  }
550
- if (!acceptedOnChain) {
551
- return rejectWith('tier requires on-chain acceptSessionDelegation blessing; not present', claims.delegation.delegator, eip712Hash);
664
+ else {
665
+ try {
666
+ acceptedOnChain = (await publicClient.readContract({
667
+ address: claims.delegation.delegator,
668
+ abi: ACCEPTED_SESSION_DELEGATION_ABI,
669
+ functionName: 'hasAcceptedSessionDelegation',
670
+ args: [eip712Hash],
671
+ }));
672
+ }
673
+ catch (e) {
674
+ return rejectWith(`acceptSessionDelegation check reverted: ${e instanceof Error ? e.message : e}`, claims.delegation.delegator, eip712Hash);
675
+ }
676
+ if (!acceptedOnChain) {
677
+ return rejectWith('tier requires on-chain acceptSessionDelegation blessing; not present', claims.delegation.delegator, eip712Hash);
678
+ }
552
679
  }
553
680
  }
554
- // 5. JTI replay
681
+ // 5. JTI replay + usage accounting.
555
682
  const limit = claims.usageLimit ?? 10;
556
- const usage = await opts.jtiStore.trackUsage(claims.jti, limit);
557
- if (!usage.allowed) {
558
- return rejectWith('token usage limit exceeded', claims.delegation.delegator, eip712Hash);
683
+ let usageHandle;
684
+ if (opts.usageMode === 'reserve') {
685
+ // Two-phase (spec 290 §3): reserve in-flight now; caller commits on success,
686
+ // releases on a later reject so a post-verify failure doesn't burn a use.
687
+ if (!opts.jtiStore.reserveUsage) {
688
+ return rejectWith("usageMode='reserve' requires a JtiStore implementing reserveUsage", claims.delegation.delegator, eip712Hash);
689
+ }
690
+ const reserved = await opts.jtiStore.reserveUsage(claims.jti, limit);
691
+ if (!reserved.allowed) {
692
+ return rejectWith('token usage limit exceeded', claims.delegation.delegator, eip712Hash);
693
+ }
694
+ usageHandle = reserved.reservation;
695
+ }
696
+ else {
697
+ const usage = await opts.jtiStore.trackUsage(claims.jti, limit);
698
+ if (!usage.allowed) {
699
+ return rejectWith('token usage limit exceeded', claims.delegation.delegator, eip712Hash);
700
+ }
559
701
  }
560
702
  // Accept emit — extended context to capture the threshold-policy
561
703
  // outcome (`acceptedOnChain`) for forensics + dashboards. Reuses the
@@ -569,19 +711,53 @@ export async function verifyDelegationToken(token, opts) {
569
711
  if (opts.requireAcceptedOnChain !== undefined) {
570
712
  context.acceptedOnChain = acceptedOnChain;
571
713
  }
572
- await opts.auditSink.write(buildEvent({
573
- action: 'delegation.verify.accept',
574
- outcome: 'success',
575
- correlationId: opts.correlationId,
576
- actor: { type: 'user', id: claims.delegation.delegator },
577
- subject: { type: 'delegation', id: eip712Hash },
578
- audience: opts.audience,
579
- chainId: opts.chainId,
580
- digest: eip712Hash,
581
- context: Object.keys(context).length > 0 ? context : undefined,
582
- }));
714
+ try {
715
+ await opts.auditSink.write(buildEvent({
716
+ action: 'delegation.verify.accept',
717
+ outcome: 'success',
718
+ correlationId: opts.correlationId,
719
+ actor: { type: 'user', id: claims.delegation.delegator },
720
+ subject: { type: 'delegation', id: eip712Hash },
721
+ audience: opts.audience,
722
+ chainId: opts.chainId,
723
+ digest: eip712Hash,
724
+ context: Object.keys(context).length > 0 ? context : undefined,
725
+ }));
726
+ }
727
+ catch (e) {
728
+ // A fail-hard audit sink throwing AFTER we reserved must not leak the
729
+ // in-flight slot — release before propagating (spec 290 §3).
730
+ if (usageHandle) {
731
+ try {
732
+ await usageHandle.release();
733
+ }
734
+ catch { /* best-effort */ }
735
+ }
736
+ throw e;
737
+ }
583
738
  }
584
- return { principal: claims.delegation.delegator };
739
+ // Off-chain DATA_SCOPE caveats (field-scoping, spec 291-A): decode any present into `grants` so the
740
+ // resource server (the vault/MCP runtime) can RESTRICT the field projection to what the issuer granted.
741
+ // The on-chain evaluator treats the sentinel as inert; enforcement is here, off-chain (ADR-0041 — the
742
+ // scope rides in the SAME signed delegation that conveys authority, so it can't be replayed without it).
743
+ // Authentic (decoded only AFTER the signature verified) — a malformed scope in a signed delegation throws.
744
+ const grants = [];
745
+ for (const cav of claims.delegation.caveats ?? []) {
746
+ if (cav.enforcer.toLowerCase() === DATA_SCOPE_ENFORCER.toLowerCase()) {
747
+ grants.push(...decodeDataScopeTerms(cav.terms));
748
+ }
749
+ }
750
+ // `sessionKeyAddress` is surfaced (additive) so callers can cross-check a
751
+ // per-call proof's signer against the bound session key (spec 287 §6). It
752
+ // equals `recovered` (verified above to match `claims.sessionKeyAddress`).
753
+ // `usage` is present only in reserve mode — the caller commits on success,
754
+ // releases on a later reject (spec 290 §3).
755
+ return {
756
+ principal: claims.delegation.delegator,
757
+ sessionKeyAddress: claims.sessionKeyAddress,
758
+ ...(grants.length ? { grants } : {}),
759
+ ...(usageHandle ? { usage: usageHandle } : {}),
760
+ };
585
761
  }
586
762
  // H7-B.8 (XPKG-002 / EXT-024 closure) — `verifyCrossDelegation` was a
587
763
  // public stub that unconditionally returned a "not implemented" error