@agenticprimitives/delegation 1.0.0-alpha.13 → 1.0.0-alpha.15
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/caveats.d.ts +3 -0
- package/dist/caveats.d.ts.map +1 -1
- package/dist/caveats.js +16 -0
- package/dist/caveats.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/invocation-proof.d.ts +133 -0
- package/dist/invocation-proof.d.ts.map +1 -0
- package/dist/invocation-proof.js +157 -0
- package/dist/invocation-proof.js.map +1 -0
- package/dist/onchain.d.ts +23 -0
- package/dist/onchain.d.ts.map +1 -1
- package/dist/onchain.js +64 -1
- package/dist/onchain.js.map +1 -1
- package/dist/quorum.d.ts +26 -0
- package/dist/quorum.d.ts.map +1 -0
- package/dist/quorum.js +80 -0
- package/dist/quorum.js.map +1 -0
- package/dist/token.d.ts +75 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +227 -51
- package/dist/token.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
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
|
package/dist/token.d.ts.map
CHANGED
|
@@ -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;
|
|
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: '
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
|
|
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.
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
557
|
-
if (
|
|
558
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|