@agenticprimitives/delegation 1.0.0-alpha.4 → 1.0.0-alpha.40
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 +19 -2
- package/dist/authority-chain.d.ts +59 -0
- package/dist/authority-chain.d.ts.map +1 -0
- package/dist/authority-chain.js +70 -0
- package/dist/authority-chain.js.map +1 -0
- package/dist/caveats.d.ts +196 -1
- package/dist/caveats.d.ts.map +1 -1
- package/dist/caveats.js +374 -2
- package/dist/caveats.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/evaluator.d.ts +1 -1
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/evaluator.js +102 -2
- package/dist/evaluator.js.map +1 -1
- package/dist/hash.d.ts +1 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/index.d.ts +38 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -8
- 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/lineage.d.ts +70 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +65 -0
- package/dist/lineage.js.map +1 -0
- package/dist/mandate-handlers.d.ts +15 -0
- package/dist/mandate-handlers.d.ts.map +1 -0
- package/dist/mandate-handlers.js +279 -0
- package/dist/mandate-handlers.js.map +1 -0
- package/dist/mandate.d.ts +265 -0
- package/dist/mandate.d.ts.map +1 -0
- package/dist/mandate.js +275 -0
- package/dist/mandate.js.map +1 -0
- package/dist/method-authorization.d.ts +120 -0
- package/dist/method-authorization.d.ts.map +1 -0
- package/dist/method-authorization.js +173 -0
- package/dist/method-authorization.js.map +1 -0
- package/dist/onchain.d.ts +40 -4
- package/dist/onchain.d.ts.map +1 -1
- package/dist/onchain.js +140 -6
- package/dist/onchain.js.map +1 -1
- package/dist/presentation-release.d.ts +65 -0
- package/dist/presentation-release.d.ts.map +1 -0
- package/dist/presentation-release.js +81 -0
- package/dist/presentation-release.js.map +1 -0
- 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/session-delegation.d.ts +34 -0
- package/dist/session-delegation.d.ts.map +1 -0
- package/dist/session-delegation.js +33 -0
- package/dist/session-delegation.js.map +1 -0
- package/dist/session-manager.d.ts +1 -2
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +15 -6
- package/dist/session-manager.js.map +1 -1
- package/dist/standing.d.ts +101 -0
- package/dist/standing.d.ts.map +1 -0
- package/dist/standing.js +132 -0
- package/dist/standing.js.map +1 -0
- package/dist/token.d.ts +89 -1
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +485 -123
- package/dist/token.js.map +1 -1
- package/dist/types.d.ts +141 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/dist/verify-authorization.d.ts +28 -0
- package/dist/verify-authorization.d.ts.map +1 -0
- package/dist/verify-authorization.js +73 -0
- package/dist/verify-authorization.js.map +1 -0
- package/package.json +25 -16
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Agentic Invocation Proof (spec 287). A per-call proof-of-possession: the
|
|
2
|
+
// DEL-001-bound session key (spec 270) signs an EIP-712 digest over the EXACT
|
|
3
|
+
// call so a public native MCP request is bound to its exact argument payload —
|
|
4
|
+
// the property A2A already has via `hashA2aMessage` and the internal mesh has
|
|
5
|
+
// via the service MAC, but which the direct public hop lacked.
|
|
6
|
+
//
|
|
7
|
+
// This module is the PURE crypto primitive: the type, the EIP-712 digest, the
|
|
8
|
+
// canonical argument/token hashing, and a claims verifier that takes an INJECTED
|
|
9
|
+
// signature verifier (the `UniversalSignatureValidator` surface — so any
|
|
10
|
+
// credential strategy works) + an INJECTED `JtiStore` (one-shot requestId). It
|
|
11
|
+
// imports no audit/transport — it returns a `{ ok, reason }` result and never
|
|
12
|
+
// emits. The MCP-specific wiring (recompute argsHash from the received call,
|
|
13
|
+
// cross-check against the delegation token, gate on `requireInvocationProof`,
|
|
14
|
+
// emit audit) lives in `mcp-runtime`'s `with-delegation` — spec 287 §9.
|
|
15
|
+
import { hashTypedData, keccak256, toBytes } from 'viem';
|
|
16
|
+
import { canonicalJSON } from './token.js';
|
|
17
|
+
export const INVOCATION_PROOF_VERSION = 'agentic-invocation-v1';
|
|
18
|
+
/** Default bounded TTL window (spec 287 §5). */
|
|
19
|
+
export const DEFAULT_MAX_PROOF_TTL_MS = 120_000;
|
|
20
|
+
export const DEFAULT_PROOF_CLOCK_SKEW_MS = 30_000;
|
|
21
|
+
export const INVOCATION_PROOF_EIP712_TYPES = {
|
|
22
|
+
AgenticInvocationProofV1: [
|
|
23
|
+
{ name: 'audience', type: 'string' },
|
|
24
|
+
{ name: 'operation', type: 'string' },
|
|
25
|
+
{ name: 'argumentsHash', type: 'bytes32' },
|
|
26
|
+
{ name: 'delegationTokenHash', type: 'bytes32' },
|
|
27
|
+
{ name: 'requestId', type: 'string' },
|
|
28
|
+
{ name: 'issuedAt', type: 'uint256' },
|
|
29
|
+
{ name: 'expiresAt', type: 'uint256' },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
/** EIP-712 domain — no `verifyingContract` (this is an off-chain proof). */
|
|
33
|
+
export function invocationProofDomain(chainId) {
|
|
34
|
+
return { name: 'AgenticInvocationProof', version: '1', chainId };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The digest the session key signs (and the verifier recomputes). Building it is
|
|
38
|
+
* pure — importable client-side to build + sign a proof.
|
|
39
|
+
*/
|
|
40
|
+
export function hashInvocationProof(p) {
|
|
41
|
+
return hashTypedData({
|
|
42
|
+
domain: invocationProofDomain(p.chainId),
|
|
43
|
+
types: INVOCATION_PROOF_EIP712_TYPES,
|
|
44
|
+
primaryType: 'AgenticInvocationProofV1',
|
|
45
|
+
message: {
|
|
46
|
+
audience: p.audience,
|
|
47
|
+
operation: p.operation,
|
|
48
|
+
argumentsHash: p.argumentsHash,
|
|
49
|
+
delegationTokenHash: p.delegationTokenHash,
|
|
50
|
+
requestId: p.requestId,
|
|
51
|
+
issuedAt: BigInt(p.issuedAt),
|
|
52
|
+
expiresAt: BigInt(p.expiresAt),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Canonical hash of the EXACT tool arguments (spec 287 §4). Uses the repo's
|
|
58
|
+
* cross-runtime-stable `canonicalJSON` so the client's signed hash and the
|
|
59
|
+
* runtime's recomputed hash are byte-identical. The runtime MUST recompute this
|
|
60
|
+
* from the args it actually received — never trust the client's stated value.
|
|
61
|
+
*/
|
|
62
|
+
export function canonicalArgumentsHash(args) {
|
|
63
|
+
return keccak256(toBytes(canonicalJSON(args)));
|
|
64
|
+
}
|
|
65
|
+
/** Bind the proof to the presented delegation token (spec 287 §4). */
|
|
66
|
+
export function hashDelegationTokenString(rawToken) {
|
|
67
|
+
return keccak256(toBytes(rawToken));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Client-side builder: assemble + sign an invocation proof. The caller supplies
|
|
71
|
+
* a `sign(digest)` that produces the session key's signature over the EIP-712
|
|
72
|
+
* digest (e.g. `account.sign({ hash })` for an EOA session key, or any SA
|
|
73
|
+
* `Signer`). `args` MUST be the exact tool arguments the call will
|
|
74
|
+
* carry — the runtime recomputes `canonicalArgumentsHash(args)` from what it
|
|
75
|
+
* receives and rejects a mismatch. Pure (no chain); importable in the browser.
|
|
76
|
+
*/
|
|
77
|
+
export async function buildInvocationProof(input) {
|
|
78
|
+
const signed = {
|
|
79
|
+
chainId: input.chainId,
|
|
80
|
+
audience: input.audience,
|
|
81
|
+
operation: input.operation,
|
|
82
|
+
argumentsHash: canonicalArgumentsHash(input.args),
|
|
83
|
+
delegationTokenHash: hashDelegationTokenString(input.rawDelegationToken),
|
|
84
|
+
requestId: input.requestId,
|
|
85
|
+
issuedAt: input.issuedAt,
|
|
86
|
+
expiresAt: input.expiresAt,
|
|
87
|
+
};
|
|
88
|
+
const signature = await input.sign(hashInvocationProof(signed));
|
|
89
|
+
return {
|
|
90
|
+
version: INVOCATION_PROOF_VERSION,
|
|
91
|
+
principal: input.principal,
|
|
92
|
+
sessionKey: input.sessionKey,
|
|
93
|
+
signature,
|
|
94
|
+
...signed,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function hexEq(a, b) {
|
|
98
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Verify an invocation proof. Cheap local checks gate first; the (potentially
|
|
102
|
+
* chain-bound) signature verify runs next; the one-shot `requestId` is reserved
|
|
103
|
+
* ONLY after the signature verifies (so an unauthenticated party can't burn a
|
|
104
|
+
* victim's requestId). Fail-closed: every failure returns `{ ok: false, reason }`.
|
|
105
|
+
* Callers MUST NOT echo `reason` to external clients — log it, return a generic
|
|
106
|
+
* error (spec 287 §5, ADR-0013).
|
|
107
|
+
*/
|
|
108
|
+
export async function verifyInvocationProofClaims(input) {
|
|
109
|
+
const { proof, expected } = input;
|
|
110
|
+
const now = (input.now ?? Date.now)();
|
|
111
|
+
const maxTtl = input.maxProofTtlMs ?? DEFAULT_MAX_PROOF_TTL_MS;
|
|
112
|
+
const skew = input.clockSkewMs ?? DEFAULT_PROOF_CLOCK_SKEW_MS;
|
|
113
|
+
if (proof.version !== INVOCATION_PROOF_VERSION)
|
|
114
|
+
return { ok: false, reason: 'version' };
|
|
115
|
+
if (proof.chainId !== expected.chainId)
|
|
116
|
+
return { ok: false, reason: 'chain_mismatch' };
|
|
117
|
+
if (proof.audience !== expected.audience)
|
|
118
|
+
return { ok: false, reason: 'audience_mismatch' };
|
|
119
|
+
if (proof.operation !== expected.operation)
|
|
120
|
+
return { ok: false, reason: 'operation_mismatch' };
|
|
121
|
+
if (!hexEq(proof.argumentsHash, expected.argumentsHash))
|
|
122
|
+
return { ok: false, reason: 'arguments_mismatch' };
|
|
123
|
+
if (!hexEq(proof.delegationTokenHash, expected.delegationTokenHash)) {
|
|
124
|
+
return { ok: false, reason: 'delegation_token_mismatch' };
|
|
125
|
+
}
|
|
126
|
+
// Bounded window (spec 287 §5).
|
|
127
|
+
if (!Number.isFinite(proof.issuedAt) || !Number.isFinite(proof.expiresAt)) {
|
|
128
|
+
return { ok: false, reason: 'window_malformed' };
|
|
129
|
+
}
|
|
130
|
+
if (proof.expiresAt <= proof.issuedAt)
|
|
131
|
+
return { ok: false, reason: 'window_inverted' };
|
|
132
|
+
if (proof.expiresAt - proof.issuedAt > maxTtl)
|
|
133
|
+
return { ok: false, reason: 'window_too_long' };
|
|
134
|
+
if (proof.issuedAt > now + skew)
|
|
135
|
+
return { ok: false, reason: 'not_yet_valid' };
|
|
136
|
+
if (now > proof.expiresAt)
|
|
137
|
+
return { ok: false, reason: 'expired' };
|
|
138
|
+
// Proof-of-possession over the exact digest. Injected verifier → any credential
|
|
139
|
+
// strategy (EOA ECDSA, ERC-1271, ERC-6492) via UniversalSignatureValidator.
|
|
140
|
+
const digest = hashInvocationProof(proof);
|
|
141
|
+
let sigOk = false;
|
|
142
|
+
try {
|
|
143
|
+
sigOk = await input.verifySignature({ signer: proof.sessionKey, digest, signature: proof.signature });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { ok: false, reason: 'signature_error' };
|
|
147
|
+
}
|
|
148
|
+
if (!sigOk)
|
|
149
|
+
return { ok: false, reason: 'signature_invalid' };
|
|
150
|
+
// One-shot requestId reservation — ONLY after the signature verified.
|
|
151
|
+
const jti = `invproof:${proof.audience}:${proof.requestId}`;
|
|
152
|
+
const tracked = await input.jtiStore.trackUsage(jti, 1);
|
|
153
|
+
if (!tracked.allowed)
|
|
154
|
+
return { ok: false, reason: 'request_replayed' };
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=invocation-proof.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invocation-proof.js","sourceRoot":"","sources":["../src/invocation-proof.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,+DAA+D;AAC/D,EAAE;AACF,8EAA8E;AAC9E,iFAAiF;AACjF,yEAAyE;AACzE,+EAA+E;AAC/E,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAC9E,wEAAwE;AAExE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,CAAC,MAAM,wBAAwB,GAAG,uBAAgC,CAAC;AAEzE,gDAAgD;AAChD,MAAM,CAAC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAChD,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAoClD,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,wBAAwB,EAAE;QACxB,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE;QACpC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE;QACrC,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE;QAC1C,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,SAAS,EAAE;QAChD,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE;QACrC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE;QACrC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE;KACvC;CACO,CAAC;AAEX,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,OAAe;IACnD,OAAO,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAW,CAAC;AAC5E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAA8B;IAChE,OAAO,aAAa,CAAC;QACnB,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,OAAO,CAAC;QACxC,KAAK,EAAE,6BAA6B;QACpC,WAAW,EAAE,0BAA0B;QACvC,OAAO,EAAE;YACP,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,mBAAmB,EAAE,CAAC,CAAC,mBAAmB;YAC1C,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;YAC5B,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;SAC/B;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAa;IAClD,OAAO,SAAS,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,yBAAyB,CAAC,QAAgB;IACxD,OAAO,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAc1C;IACC,MAAM,MAAM,GAAgC;QAC1C,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,aAAa,EAAE,sBAAsB,CAAC,KAAK,CAAC,IAAI,CAAC;QACjD,mBAAmB,EAAE,yBAAyB,CAAC,KAAK,CAAC,kBAAkB,CAAC;QACxE,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,SAAS;KAC3B,CAAC;IACF,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,OAAO;QACL,OAAO,EAAE,wBAAwB;QACjC,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,SAAS;QACT,GAAG,MAAM;KACV,CAAC;AACJ,CAAC;AA4BD,SAAS,KAAK,CAAC,CAAS,EAAE,CAAS;IACjC,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;AAC7C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,KAAiC;IAEjC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAClC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,IAAI,wBAAwB,CAAC;IAC/D,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,IAAI,2BAA2B,CAAC;IAE9D,IAAI,KAAK,CAAC,OAAO,KAAK,wBAAwB;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACxF,IAAI,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IACvF,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC5F,IAAI,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/F,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,EAAE,QAAQ,CAAC,aAAa,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC5G,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,mBAAmB,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACpE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;IAC5D,CAAC;IAED,gCAAgC;IAChC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IACnD,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACvF,IAAI,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,QAAQ,GAAG,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC/F,IAAI,KAAK,CAAC,QAAQ,GAAG,GAAG,GAAG,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC/E,IAAI,GAAG,GAAG,KAAK,CAAC,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAEnE,gFAAgF;IAChF,4EAA4E;IAC5E,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;IACxG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAE9D,sEAAsE;IACtE,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACxD,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAEvE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Address, Hex } from './types.js';
|
|
2
|
+
/** The JSON-safe wire (salt as a string), as it travels and as a vault record holds it. */
|
|
3
|
+
export interface LineageWire {
|
|
4
|
+
delegator: Address;
|
|
5
|
+
delegate: Address;
|
|
6
|
+
authority: Hex;
|
|
7
|
+
caveats: Array<{
|
|
8
|
+
enforcer: Address;
|
|
9
|
+
terms: Hex;
|
|
10
|
+
args?: Hex;
|
|
11
|
+
}>;
|
|
12
|
+
salt: string;
|
|
13
|
+
signature: Hex;
|
|
14
|
+
}
|
|
15
|
+
/** The account-level approved-digest sentinel (spec 253): the delegator's account has `approveDigest`ed the
|
|
16
|
+
* wire's hash under its current custody epoch, and `isValidSignature` honours it. */
|
|
17
|
+
export declare const APPROVED_DIGEST_SENTINEL: Hex;
|
|
18
|
+
export interface DelegationLineageV1 {
|
|
19
|
+
type: 'ap.delegation-lineage.v1';
|
|
20
|
+
/** The re-issued wire's delegation hash — the record key's suffix. */
|
|
21
|
+
hash: Hex;
|
|
22
|
+
/** The hash this wire replaces; receipts citing it remain readable through this record. */
|
|
23
|
+
supersedes: Hex;
|
|
24
|
+
/** The re-issued wire itself: same terms, same salt, `0x03` signature. Returned to its delegate only. */
|
|
25
|
+
wire: LineageWire;
|
|
26
|
+
issuedAt: string;
|
|
27
|
+
/** Why it was re-issued. A rotation is the only reason today; a recovery re-issues NOTHING (spec 410 §1.2 step 6) —
|
|
28
|
+
* the person issues NEW grants from a reviewed list, with new salts and no lineage, because a recovery is the
|
|
29
|
+
* moment a planted grant would otherwise ride through. */
|
|
30
|
+
reason: 'rotation';
|
|
31
|
+
}
|
|
32
|
+
export declare const LINEAGE_RECORD_PREFIX: "delegation.lineage:";
|
|
33
|
+
export declare const lineageRecordType: (hash: Hex) => string;
|
|
34
|
+
/** Is this wire signed by the account's approved-digest sentinel (re-approved as-is on rotation) or by a key
|
|
35
|
+
* (re-issued on rotation)? */
|
|
36
|
+
export declare const wireSigning: (w: {
|
|
37
|
+
signature: string;
|
|
38
|
+
}) => "approved-digest" | "key";
|
|
39
|
+
/**
|
|
40
|
+
* Re-issue a KEY-SIGNED wire as an approved-digest wire for a rotation: identical terms, identical salt, the
|
|
41
|
+
* `0x03` sentinel as signature. The digest returned is what the rotation batch `approveDigest`s; the lineage
|
|
42
|
+
* record is what the delegator's vault keeps so the delegate can fetch the head. Refuses a wire that is
|
|
43
|
+
* already approved-digest (it needs re-approval, not re-issue) and one whose `supersedes` would be itself.
|
|
44
|
+
*/
|
|
45
|
+
export declare function reissueForRotation(old: LineageWire, chainId: number, delegationManager: Address, now?: () => Date): {
|
|
46
|
+
wire: LineageWire;
|
|
47
|
+
digest: Hex;
|
|
48
|
+
lineage: DelegationLineageV1;
|
|
49
|
+
};
|
|
50
|
+
export type WireLineageAnswer = {
|
|
51
|
+
status: 'current';
|
|
52
|
+
hash: Hex;
|
|
53
|
+
wire: LineageWire | null;
|
|
54
|
+
} | {
|
|
55
|
+
status: 'superseded';
|
|
56
|
+
hash: Hex;
|
|
57
|
+
wire: LineageWire;
|
|
58
|
+
supersededFrom: Hex;
|
|
59
|
+
} | {
|
|
60
|
+
status: 'unknown';
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The HEAD of a wire's lineage, from the delegator's lineage records: follow `supersedes` links from `hash`
|
|
64
|
+
* forward until no record supersedes the current head. Pure; the caller supplies the records (a vault list) and
|
|
65
|
+
* the delegate asking, and a record whose wire names another delegate is never followed — a wire is returned to
|
|
66
|
+
* its delegate only. `unknown` when nothing in the records mentions the hash: the caller then decides whether
|
|
67
|
+
* the ORIGINAL is still current (it holds it) or was revoked (the chain says).
|
|
68
|
+
*/
|
|
69
|
+
export declare function currentWireOf(records: readonly DelegationLineageV1[], hash: Hex, delegate: Address): WireLineageAnswer;
|
|
70
|
+
//# sourceMappingURL=lineage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../src/lineage.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAI/C,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,GAAG,CAAC;IACf,OAAO,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,GAAG,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,GAAG,CAAC;CAChB;AAED;sFACsF;AACtF,eAAO,MAAM,wBAAwB,EAAE,GAAY,CAAC;AAEpD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,0BAA0B,CAAC;IACjC,sEAAsE;IACtE,IAAI,EAAE,GAAG,CAAC;IACV,2FAA2F;IAC3F,UAAU,EAAE,GAAG,CAAC;IAChB,yGAAyG;IACzG,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB;;+DAE2D;IAC3D,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,eAAO,MAAM,qBAAqB,EAAG,qBAA8B,CAAC;AACpE,eAAO,MAAM,iBAAiB,SAAU,GAAG,KAAG,MAAyD,CAAC;AAQxG;+BAC+B;AAC/B,eAAO,MAAM,WAAW,MAAO;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,KAAG,iBAAiB,GAAG,KACS,CAAC;AAErF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,GAAG,GAAE,MAAM,IAAuB,GAAG;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE,CAUtM;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,GAC1D;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,cAAc,EAAE,GAAG,CAAA;CAAE,GAC3E;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAE1B;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,mBAAmB,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,GAAG,iBAAiB,CAqBtH"}
|
package/dist/lineage.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { hashDelegation } from './hash.js';
|
|
2
|
+
/** The account-level approved-digest sentinel (spec 253): the delegator's account has `approveDigest`ed the
|
|
3
|
+
* wire's hash under its current custody epoch, and `isValidSignature` honours it. */
|
|
4
|
+
export const APPROVED_DIGEST_SENTINEL = '0x03';
|
|
5
|
+
export const LINEAGE_RECORD_PREFIX = 'delegation.lineage:';
|
|
6
|
+
export const lineageRecordType = (hash) => `${LINEAGE_RECORD_PREFIX}${hash.toLowerCase()}`;
|
|
7
|
+
const toDelegation = (w) => ({
|
|
8
|
+
delegator: w.delegator, delegate: w.delegate, authority: w.authority,
|
|
9
|
+
caveats: w.caveats.map((c) => ({ enforcer: c.enforcer, terms: c.terms, args: (c.args ?? '0x') })),
|
|
10
|
+
salt: BigInt(w.salt), signature: w.signature,
|
|
11
|
+
});
|
|
12
|
+
/** Is this wire signed by the account's approved-digest sentinel (re-approved as-is on rotation) or by a key
|
|
13
|
+
* (re-issued on rotation)? */
|
|
14
|
+
export const wireSigning = (w) => w.signature.toLowerCase() === APPROVED_DIGEST_SENTINEL ? 'approved-digest' : 'key';
|
|
15
|
+
/**
|
|
16
|
+
* Re-issue a KEY-SIGNED wire as an approved-digest wire for a rotation: identical terms, identical salt, the
|
|
17
|
+
* `0x03` sentinel as signature. The digest returned is what the rotation batch `approveDigest`s; the lineage
|
|
18
|
+
* record is what the delegator's vault keeps so the delegate can fetch the head. Refuses a wire that is
|
|
19
|
+
* already approved-digest (it needs re-approval, not re-issue) and one whose `supersedes` would be itself.
|
|
20
|
+
*/
|
|
21
|
+
export function reissueForRotation(old, chainId, delegationManager, now = () => new Date()) {
|
|
22
|
+
if (wireSigning(old) === 'approved-digest')
|
|
23
|
+
throw new Error('reissueForRotation: an approved-digest wire is re-approved as-is on rotation, never re-issued');
|
|
24
|
+
const oldHash = hashDelegation(toDelegation(old), chainId, delegationManager);
|
|
25
|
+
const wire = { ...old, caveats: old.caveats.map((c) => ({ ...c })), signature: APPROVED_DIGEST_SENTINEL };
|
|
26
|
+
const digest = hashDelegation(toDelegation(wire), chainId, delegationManager);
|
|
27
|
+
// The EIP-712 hash excludes the signature, so the re-issued object HAS THE SAME DELEGATION HASH as the old one:
|
|
28
|
+
// what changes is how it validates (the sentinel, under the new epoch), not what it grants. The lineage still
|
|
29
|
+
// records the supersession so the delegate learns the object it holds is stale and which one to hold now.
|
|
30
|
+
const lineage = { type: 'ap.delegation-lineage.v1', hash: digest, supersedes: oldHash, wire, issuedAt: now().toISOString(), reason: 'rotation' };
|
|
31
|
+
return { wire, digest, lineage };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The HEAD of a wire's lineage, from the delegator's lineage records: follow `supersedes` links from `hash`
|
|
35
|
+
* forward until no record supersedes the current head. Pure; the caller supplies the records (a vault list) and
|
|
36
|
+
* the delegate asking, and a record whose wire names another delegate is never followed — a wire is returned to
|
|
37
|
+
* its delegate only. `unknown` when nothing in the records mentions the hash: the caller then decides whether
|
|
38
|
+
* the ORIGINAL is still current (it holds it) or was revoked (the chain says).
|
|
39
|
+
*/
|
|
40
|
+
export function currentWireOf(records, hash, delegate) {
|
|
41
|
+
const byPrev = new Map();
|
|
42
|
+
for (const r of records) {
|
|
43
|
+
if (r.type !== 'ap.delegation-lineage.v1')
|
|
44
|
+
continue;
|
|
45
|
+
if (r.wire.delegate.toLowerCase() !== delegate.toLowerCase())
|
|
46
|
+
continue;
|
|
47
|
+
byPrev.set(r.supersedes.toLowerCase(), r);
|
|
48
|
+
}
|
|
49
|
+
let head = hash.toLowerCase();
|
|
50
|
+
let last = null;
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
while (byPrev.has(head)) {
|
|
53
|
+
if (seen.has(head))
|
|
54
|
+
break; // a cycle is a corrupt record set; stop at the last sound link
|
|
55
|
+
seen.add(head);
|
|
56
|
+
last = byPrev.get(head);
|
|
57
|
+
head = last.hash.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
if (!last) {
|
|
60
|
+
const known = records.some((r) => r.hash.toLowerCase() === hash.toLowerCase() && r.wire.delegate.toLowerCase() === delegate.toLowerCase());
|
|
61
|
+
return known ? { status: 'current', hash, wire: records.find((r) => r.hash.toLowerCase() === hash.toLowerCase()).wire } : { status: 'unknown' };
|
|
62
|
+
}
|
|
63
|
+
return { status: 'superseded', hash: last.hash, wire: last.wire, supersededFrom: hash };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=lineage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineage.js","sourceRoot":"","sources":["../src/lineage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type MandateSubsetHandler } from './mandate.js';
|
|
2
|
+
export declare const CAPABILITY_RAR_TYPE = "urn:ap:rar:capability";
|
|
3
|
+
export declare const capabilityHandler: MandateSubsetHandler;
|
|
4
|
+
export declare const PAYMENT_RAR_TYPE = "urn:ap:rar:treasury.payment.execute";
|
|
5
|
+
export declare const paymentHandler: MandateSubsetHandler;
|
|
6
|
+
export declare const PRESENTATION_RAR_TYPE = "urn:ap:rar:credential.present";
|
|
7
|
+
export declare const PRESENTATION_CAPABILITY = "credential.present";
|
|
8
|
+
export declare const presentationHandler: MandateSubsetHandler;
|
|
9
|
+
/** The defaults as an explicit set — pass this to `verifyMandateForStep({ handlers })` rather than relying
|
|
10
|
+
* on ambient registration, which does not survive a bundle that duplicates the module. */
|
|
11
|
+
export declare const DEFAULT_SUBSET_HANDLERS: ReadonlyMap<string, MandateSubsetHandler>;
|
|
12
|
+
export declare function registerDefaultSubsetHandlers(): void;
|
|
13
|
+
/** Test seam — pairs with `_resetSubsetHandlers`. */
|
|
14
|
+
export declare function _resetDefaultRegistration(): void;
|
|
15
|
+
//# sourceMappingURL=mandate-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mandate-handlers.d.ts","sourceRoot":"","sources":["../src/mandate-handlers.ts"],"names":[],"mappings":"AAaA,OAAO,EAA0F,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAYjJ,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAE3D,eAAO,MAAM,iBAAiB,EAAE,oBAwC/B,CAAC;AAsBF,eAAO,MAAM,gBAAgB,wCAAwC,CAAC;AActE,eAAO,MAAM,cAAc,EAAE,oBAkE5B,CAAC;AAqBF,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AACrE,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAkB5D,eAAO,MAAM,mBAAmB,EAAE,oBA+CjC,CAAC;AAEF;2FAC2F;AAC3F,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,EAAE,oBAAoB,CAI5E,CAAC;AAIH,wBAAgB,6BAA6B,IAAI,IAAI,CAMpD;AACD,qDAAqD;AACrD,wBAAgB,yBAAyB,IAAI,IAAI,CAAwB"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// The first subset handlers — spec 350 §2.1. Each `type` owns its own "narrower-than", because RFC 9396
|
|
2
|
+
// says no universal one exists. Two ship here: a GENERIC capability handler that covers every
|
|
3
|
+
// non-financial action (membership, naming, registry), and the PAYMENT handler that reuses the
|
|
4
|
+
// spec-272 `PaymentEnforcer` caveats so a treasury mandate is bounded on chain by the same terms the
|
|
5
|
+
// off-chain verifier reads.
|
|
6
|
+
import { decodeAbiParameters } from 'viem';
|
|
7
|
+
import { buildCaveat, encodeTimestampTerms, decodeTimestampTerms, encodeAllowedMethodsTerms, decodeAllowedMethodsTerms, encodeAllowedTargetsTerms, decodeAllowedTargetsTerms, encodeValueTerms, encodePaymentTerms, PAYMENT_TRANSFER_SELECTOR, buildPayloadClassesCaveat, decodePayloadClassesTerms, PRESENTATION_SCOPE_ENFORCER, buildPresentationScopeCaveat, decodePresentationScopeTerms, } from './caveats.js';
|
|
8
|
+
import { methodSelector } from './method-authorization.js';
|
|
9
|
+
import { baseIsSubset, registerSubsetHandler } from './mandate.js';
|
|
10
|
+
const lc = (s) => s.toLowerCase();
|
|
11
|
+
const findCaveat = (caveats, enforcer) => enforcer ? caveats.find((c) => lc(c.enforcer) === lc(enforcer)) : undefined;
|
|
12
|
+
// ─── Generic capability handler ────────────────────────────────────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// type: `urn:ap:rar:capability`. actions = capability ids (ADR-0051), each becoming an allowedMethods
|
|
15
|
+
// selector; locations = the agent SAs this authority may be exercised AGAINST (allowedTargets); a window;
|
|
16
|
+
// value pinned to 0. No type-specific limits — a membership add either is or is not in scope.
|
|
17
|
+
export const CAPABILITY_RAR_TYPE = 'urn:ap:rar:capability';
|
|
18
|
+
export const capabilityHandler = {
|
|
19
|
+
type: CAPABILITY_RAR_TYPE,
|
|
20
|
+
isSubset: baseIsSubset,
|
|
21
|
+
toCaveats(req, e) {
|
|
22
|
+
if (!req.actions.length)
|
|
23
|
+
throw new Error('capability mandate: at least one action (capability id) is required');
|
|
24
|
+
const caveats = [
|
|
25
|
+
buildCaveat(e.timestamp, encodeTimestampTerms(req.validAfter ?? 0, req.validUntil)),
|
|
26
|
+
// Capability selectors (what the off-chain verifier compares) AND the real EVM selectors this
|
|
27
|
+
// authority actually calls (what the enforcer sees). Both are "methods allowed"; omitting the
|
|
28
|
+
// second made every on-chain capability call revert MethodNotAllowed.
|
|
29
|
+
buildCaveat(e.allowedMethods, encodeAllowedMethodsTerms([...req.actions.map(methodSelector), ...(req.methods ?? [])])),
|
|
30
|
+
buildCaveat(e.value, encodeValueTerms(0n)),
|
|
31
|
+
];
|
|
32
|
+
if (req.locations?.length)
|
|
33
|
+
caveats.push(buildCaveat(e.allowedTargets, encodeAllowedTargetsTerms(req.locations.map(asAddress))));
|
|
34
|
+
// Spec 410 §7 — what a write under this mandate may carry, pinned into the signed wire.
|
|
35
|
+
if (req.payloadClasses?.length) {
|
|
36
|
+
if (!e.payloadClasses)
|
|
37
|
+
throw new Error('capability mandate: payloadClasses named but this estate has no PayloadClassesEnforcer');
|
|
38
|
+
caveats.push(buildPayloadClassesCaveat(e.payloadClasses, req.payloadClasses));
|
|
39
|
+
}
|
|
40
|
+
return caveats;
|
|
41
|
+
},
|
|
42
|
+
fromCaveats(caveats, e) {
|
|
43
|
+
const ts = findCaveat(caveats, e.timestamp);
|
|
44
|
+
const methods = findCaveat(caveats, e.allowedMethods);
|
|
45
|
+
if (!ts || !methods)
|
|
46
|
+
return null;
|
|
47
|
+
const payload = e.payloadClasses ? findCaveat(caveats, e.payloadClasses) : undefined;
|
|
48
|
+
const { validAfter, validUntil } = decodeTimestampTerms(ts.terms);
|
|
49
|
+
const selectors = decodeAllowedMethodsTerms(methods.terms);
|
|
50
|
+
// Selectors are one-way; the reader keeps them as `sel:<hex>` unless it can name them. A verifier
|
|
51
|
+
// compares by selector (methodSelector(step.capability)), so this is lossless for its purpose.
|
|
52
|
+
const targets = findCaveat(caveats, e.allowedTargets);
|
|
53
|
+
return {
|
|
54
|
+
type: CAPABILITY_RAR_TYPE,
|
|
55
|
+
actions: selectors.map((s) => `sel:${s}`),
|
|
56
|
+
...(targets ? { locations: [...decodeAllowedTargetsTerms(targets.terms)] } : {}),
|
|
57
|
+
...(payload ? { payloadClasses: decodePayloadClassesTerms(payload.terms) } : {}),
|
|
58
|
+
validAfter: Number(validAfter),
|
|
59
|
+
validUntil: Number(validUntil),
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/** A capability mandate grants capability IDS: a step exercising `organization.team.create` with the verb
|
|
64
|
+
* `create` is judged on the id (the verb is the capability's own facet, not a separate grant). */
|
|
65
|
+
capabilityHandler.stepActions = (step) => [step.capability];
|
|
66
|
+
/** `isSubset` for the capability type must compare ACTIONS BY SELECTOR, because a read-back mandate
|
|
67
|
+
* carries `sel:<hex>` while a step or a fresh requirement carries the capability id. */
|
|
68
|
+
capabilityHandler.isSubset = (child, parent) => {
|
|
69
|
+
const norm = (a) => (a.startsWith('sel:') ? a.slice(4).toLowerCase() : methodSelector(a).toLowerCase());
|
|
70
|
+
return baseIsSubset({ ...child, actions: child.actions.map(norm) }, { ...parent, actions: parent.actions.map(norm) });
|
|
71
|
+
};
|
|
72
|
+
// ─── Payment handler ───────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
//
|
|
74
|
+
// type: `urn:ap:rar:treasury.payment.execute`. limits = { payee, asset, maxAmount, maxAggregate,
|
|
75
|
+
// maxRedemptionsPerWindow, windowSeconds }; the on-chain terms are the spec-272 PaymentEnforcer's.
|
|
76
|
+
// "Pay Ligonier $250 but not Bob $250" is `payee` + `maxAmount`, not a different method.
|
|
77
|
+
export const PAYMENT_RAR_TYPE = 'urn:ap:rar:treasury.payment.execute';
|
|
78
|
+
function paymentLimits(req) {
|
|
79
|
+
const l = req.limits ?? {};
|
|
80
|
+
const need = (k) => { const v = l[k]; if (v === undefined)
|
|
81
|
+
throw new Error(`payment mandate: limits.${k} is required`); return String(v); };
|
|
82
|
+
return {
|
|
83
|
+
payee: asAddress(need('payee')), asset: asAddress(need('asset')),
|
|
84
|
+
maxAmount: BigInt(need('maxAmount')), maxAggregate: BigInt(l.maxAggregate ?? need('maxAmount')),
|
|
85
|
+
maxRedemptionsPerWindow: Number(l.maxRedemptionsPerWindow ?? 1), windowSeconds: Number(l.windowSeconds ?? 86_400),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export const paymentHandler = {
|
|
89
|
+
// Spec 408 §2.2 — the PaymentEnforcer's terms (payee, asset, per-charge and aggregate ceilings, window) are
|
|
90
|
+
// what `isSubset` below compares against the step; off chain there is no other judge of them.
|
|
91
|
+
judgesEnforcers: (e) => (e.payment ? [e.payment] : []),
|
|
92
|
+
type: PAYMENT_RAR_TYPE,
|
|
93
|
+
// The narrowest grant that covers this payment: THIS payee, THIS asset, THIS amount, once, within the
|
|
94
|
+
// hour. A person asked to authorize a payment is being asked for an amount — a requirement that omits
|
|
95
|
+
// it either cannot be minted (the enforcer needs the ceiling) or would have to invent one, and an
|
|
96
|
+
// invented ceiling is a bigger grant than anybody agreed to.
|
|
97
|
+
stepLimits(step) {
|
|
98
|
+
const a = step.args ?? {};
|
|
99
|
+
const amount = a.amount !== undefined ? String(a.amount).trim() : undefined;
|
|
100
|
+
if (amount === undefined || amount === '')
|
|
101
|
+
throw new Error('payment step: args.amount is required to bound the authority it needs');
|
|
102
|
+
// AND IT MUST BE A NUMBER. A planner asked for a payment with no figure filled the amount with the
|
|
103
|
+
// literal string "<UNKNOWN>", which is present, non-empty, and would have been encoded as the
|
|
104
|
+
// ceiling of a real mandate. A limit that cannot be read as a quantity does not bound anything, and
|
|
105
|
+
// the one thing a payment caveat exists to do is bound the quantity.
|
|
106
|
+
if (!/^\d+$/.test(amount)) {
|
|
107
|
+
throw new Error(`payment step: args.amount must be a whole number of the token's smallest units, got "${amount}"`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
payee: String(a.payee ?? ''), asset: String(a.asset ?? step.resource ?? ''),
|
|
111
|
+
maxAmount: amount, maxAggregate: amount, maxRedemptionsPerWindow: 1, windowSeconds: 3600,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
isSubset(child, parent) {
|
|
115
|
+
if (!baseIsSubset(child, parent))
|
|
116
|
+
return false;
|
|
117
|
+
// A STEP arrives with `amount` + `payee` (+ maybe `asset`); a REQUIREMENT with the full limit set.
|
|
118
|
+
// Compare what is present; anything the child leaves unstated inherits the parent's bound.
|
|
119
|
+
const p = paymentLimits(parent);
|
|
120
|
+
const cl = child.limits ?? {};
|
|
121
|
+
const amount = cl.amount !== undefined ? BigInt(String(cl.amount)) : cl.maxAmount !== undefined ? BigInt(String(cl.maxAmount)) : null;
|
|
122
|
+
if (amount !== null && amount > p.maxAmount)
|
|
123
|
+
return false;
|
|
124
|
+
if (cl.maxAggregate !== undefined && BigInt(String(cl.maxAggregate)) > p.maxAggregate)
|
|
125
|
+
return false;
|
|
126
|
+
if (cl.payee !== undefined && lc(String(cl.payee)) !== lc(p.payee))
|
|
127
|
+
return false;
|
|
128
|
+
if (cl.asset !== undefined && lc(String(cl.asset)) !== lc(p.asset))
|
|
129
|
+
return false;
|
|
130
|
+
if (cl.maxRedemptionsPerWindow !== undefined && Number(cl.maxRedemptionsPerWindow) > p.maxRedemptionsPerWindow)
|
|
131
|
+
return false;
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
134
|
+
toCaveats(req, e) {
|
|
135
|
+
if (!e.payment)
|
|
136
|
+
throw new Error('payment mandate: enforcers.payment (PaymentEnforcer) is required');
|
|
137
|
+
const p = paymentLimits(req);
|
|
138
|
+
return [
|
|
139
|
+
buildCaveat(e.payment, encodePaymentTerms({ payee: p.payee, asset: p.asset, maxAmountPerCharge: p.maxAmount, maxAggregate: p.maxAggregate, maxRedemptionsPerWindow: p.maxRedemptionsPerWindow, windowSeconds: p.windowSeconds })),
|
|
140
|
+
buildCaveat(e.timestamp, encodeTimestampTerms(req.validAfter ?? 0, req.validUntil)),
|
|
141
|
+
buildCaveat(e.allowedTargets, encodeAllowedTargetsTerms([p.asset])),
|
|
142
|
+
buildCaveat(e.allowedMethods, encodeAllowedMethodsTerms([PAYMENT_TRANSFER_SELECTOR, methodSelector('treasury.payment.execute')])),
|
|
143
|
+
];
|
|
144
|
+
},
|
|
145
|
+
fromCaveats(caveats, e) {
|
|
146
|
+
const pay = findCaveat(caveats, e.payment);
|
|
147
|
+
const ts = findCaveat(caveats, e.timestamp);
|
|
148
|
+
if (!pay || !ts)
|
|
149
|
+
return null;
|
|
150
|
+
const [payee, asset, maxAmount, maxAggregate, perWindow, windowSeconds] = decodeAbiParameters([{ type: 'address' }, { type: 'address' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint32' }, { type: 'uint32' }], pay.terms);
|
|
151
|
+
const { validAfter, validUntil } = decodeTimestampTerms(ts.terms);
|
|
152
|
+
return {
|
|
153
|
+
type: PAYMENT_RAR_TYPE,
|
|
154
|
+
actions: ['execute'],
|
|
155
|
+
locations: [asset],
|
|
156
|
+
limits: { payee, asset, maxAmount: maxAmount.toString(), maxAggregate: maxAggregate.toString(), maxRedemptionsPerWindow: perWindow, windowSeconds },
|
|
157
|
+
validAfter: Number(validAfter), validUntil: Number(validUntil),
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
function asAddress(s) {
|
|
162
|
+
const m = /0x[0-9a-fA-F]{40}$/.exec(s); // accepts a bare address or a CAIP-10 `eip155:<chain>:0x…`
|
|
163
|
+
if (!m)
|
|
164
|
+
throw new Error(`not an address or CAIP-10 account: ${s}`);
|
|
165
|
+
return m[0];
|
|
166
|
+
}
|
|
167
|
+
// ─── Presentation handler (spec 305 W2 / zkID alignment §3, wave Z2) ────────────────────────────────────────
|
|
168
|
+
//
|
|
169
|
+
// type: `urn:ap:rar:credential.present`. limits = { audiences, predicateDigests, issuerKeyIds (JSON arrays),
|
|
170
|
+
// maxPresentations }. The port of smart-agent's `SessionGrantV1.scope` (verifiers, credentialTypes,
|
|
171
|
+
// presentationDefinitionIds) + `evaluateProofPolicy` (requested predicates ⊆ allowed): what a delegate key may
|
|
172
|
+
// cause the person's DEVICE KEY to sign a `Show` challenge for. A step arrives with ONE audience, ONE predicate
|
|
173
|
+
// digest and ONE issuer keyId; the mandate carries sets. Nothing here reaches the chain — the sentinel terms are
|
|
174
|
+
// in the signed hash, and the KEY RELEASE (`presentation-release.ts`) is the judge, before the signature exists.
|
|
175
|
+
//
|
|
176
|
+
// Why by digest and not by claim: "age ≥ 18 to venue.example" and "income ≥ 100k to lender.example" are two
|
|
177
|
+
// predicate programs; a wire for the first must not sign the second (zkID alignment §3 rule 2). The digest is
|
|
178
|
+
// over the compiled, canonical program the verifier itself recomputes, so a translated permission cannot widen.
|
|
179
|
+
export const PRESENTATION_RAR_TYPE = 'urn:ap:rar:credential.present';
|
|
180
|
+
export const PRESENTATION_CAPABILITY = 'credential.present';
|
|
181
|
+
const list = (v, what) => {
|
|
182
|
+
if (Array.isArray(v))
|
|
183
|
+
return v.map(String);
|
|
184
|
+
if (typeof v === 'string') {
|
|
185
|
+
try {
|
|
186
|
+
const a = JSON.parse(v);
|
|
187
|
+
if (Array.isArray(a))
|
|
188
|
+
return a.map(String);
|
|
189
|
+
}
|
|
190
|
+
catch { /* a single value */ }
|
|
191
|
+
return v ? [v] : [];
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`presentation mandate: limits.${what} must be a list`);
|
|
194
|
+
};
|
|
195
|
+
function presentationLimits(req) {
|
|
196
|
+
const l = req.limits ?? {};
|
|
197
|
+
const out = { audiences: list(l.audiences ?? l.audience, 'audiences'), predicateDigests: list(l.predicateDigests ?? l.predicateDigest, 'predicateDigests').map(lc), issuerKeyIds: list(l.issuerKeyIds ?? l.issuerKeyId, 'issuerKeyIds'), maxPresentations: Number(l.maxPresentations ?? 0) };
|
|
198
|
+
if (!out.audiences.length || !out.predicateDigests.length || !out.issuerKeyIds.length)
|
|
199
|
+
throw new Error('presentation mandate: audiences, predicateDigests and issuerKeyIds are each required and non-empty');
|
|
200
|
+
for (const d of out.predicateDigests)
|
|
201
|
+
if (!/^0x[0-9a-f]{64}$/.test(d))
|
|
202
|
+
throw new Error(`presentation mandate: predicateDigest ${d} is not a 32-byte hex digest`);
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
export const presentationHandler = {
|
|
206
|
+
type: PRESENTATION_RAR_TYPE,
|
|
207
|
+
// The scope sentinel's terms are what `isSubset` compares; no chain judge exists for them, and none is wanted.
|
|
208
|
+
judgesEnforcers: () => [PRESENTATION_SCOPE_ENFORCER],
|
|
209
|
+
stepActions: () => ['present'],
|
|
210
|
+
stepLimits(step) {
|
|
211
|
+
const a = step.args ?? {};
|
|
212
|
+
for (const k of ['audience', 'predicateDigest', 'issuerKeyId']) {
|
|
213
|
+
if (typeof a[k] !== 'string' || !a[k])
|
|
214
|
+
throw new Error(`presentation step: args.${k} is required to bound the authority it needs`);
|
|
215
|
+
}
|
|
216
|
+
return { audiences: String(a.audience), predicateDigests: lc(String(a.predicateDigest)), issuerKeyIds: String(a.issuerKeyId), maxPresentations: 1 };
|
|
217
|
+
},
|
|
218
|
+
isSubset(child, parent) {
|
|
219
|
+
if (!baseIsSubset(child, parent))
|
|
220
|
+
return false;
|
|
221
|
+
const p = presentationLimits(parent);
|
|
222
|
+
const cl = child.limits ?? {};
|
|
223
|
+
const want = { audiences: list(cl.audiences ?? cl.audience ?? [], 'audiences'), predicateDigests: list(cl.predicateDigests ?? cl.predicateDigest ?? [], 'predicateDigests').map(lc), issuerKeyIds: list(cl.issuerKeyIds ?? cl.issuerKeyId ?? [], 'issuerKeyIds') };
|
|
224
|
+
// A child that names nothing for a dimension inherits the parent's bound; one that names something must name
|
|
225
|
+
// a subset. Audiences are compared as exact strings — a verifier's origin is not a prefix of another's.
|
|
226
|
+
if (!want.audiences.every((x) => p.audiences.includes(x)))
|
|
227
|
+
return false;
|
|
228
|
+
if (!want.predicateDigests.every((x) => p.predicateDigests.includes(x)))
|
|
229
|
+
return false;
|
|
230
|
+
if (!want.issuerKeyIds.every((x) => p.issuerKeyIds.includes(x)))
|
|
231
|
+
return false;
|
|
232
|
+
if (cl.maxPresentations !== undefined && p.maxPresentations !== 0 && Number(cl.maxPresentations) > p.maxPresentations)
|
|
233
|
+
return false;
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
toCaveats(req, e) {
|
|
237
|
+
const p = presentationLimits(req);
|
|
238
|
+
return [
|
|
239
|
+
buildPresentationScopeCaveat({ audiences: p.audiences, predicateDigests: p.predicateDigests, issuerKeyIds: p.issuerKeyIds, maxPresentations: p.maxPresentations }),
|
|
240
|
+
buildCaveat(e.timestamp, encodeTimestampTerms(req.validAfter ?? 0, req.validUntil)),
|
|
241
|
+
buildCaveat(e.allowedMethods, encodeAllowedMethodsTerms([methodSelector(PRESENTATION_CAPABILITY)])),
|
|
242
|
+
buildCaveat(e.value, encodeValueTerms(0n)),
|
|
243
|
+
];
|
|
244
|
+
},
|
|
245
|
+
fromCaveats(caveats, e) {
|
|
246
|
+
const scope = findCaveat(caveats, PRESENTATION_SCOPE_ENFORCER);
|
|
247
|
+
const ts = findCaveat(caveats, e.timestamp);
|
|
248
|
+
if (!scope || !ts)
|
|
249
|
+
return null;
|
|
250
|
+
const t = decodePresentationScopeTerms(scope.terms);
|
|
251
|
+
const { validAfter, validUntil } = decodeTimestampTerms(ts.terms);
|
|
252
|
+
return {
|
|
253
|
+
type: PRESENTATION_RAR_TYPE,
|
|
254
|
+
actions: ['present'],
|
|
255
|
+
limits: { audiences: JSON.stringify(t.audiences), predicateDigests: JSON.stringify(t.predicateDigests), issuerKeyIds: JSON.stringify(t.issuerKeyIds), maxPresentations: t.maxPresentations },
|
|
256
|
+
validAfter: Number(validAfter), validUntil: Number(validUntil),
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
/** The defaults as an explicit set — pass this to `verifyMandateForStep({ handlers })` rather than relying
|
|
261
|
+
* on ambient registration, which does not survive a bundle that duplicates the module. */
|
|
262
|
+
export const DEFAULT_SUBSET_HANDLERS = new Map([
|
|
263
|
+
[capabilityHandler.type, capabilityHandler],
|
|
264
|
+
[paymentHandler.type, paymentHandler],
|
|
265
|
+
[presentationHandler.type, presentationHandler],
|
|
266
|
+
]);
|
|
267
|
+
/** Register both into the ambient registry. Idempotent per process; a second call is a no-op. */
|
|
268
|
+
let registered = false;
|
|
269
|
+
export function registerDefaultSubsetHandlers() {
|
|
270
|
+
if (registered)
|
|
271
|
+
return;
|
|
272
|
+
registerSubsetHandler(capabilityHandler);
|
|
273
|
+
registerSubsetHandler(paymentHandler);
|
|
274
|
+
registerSubsetHandler(presentationHandler);
|
|
275
|
+
registered = true;
|
|
276
|
+
}
|
|
277
|
+
/** Test seam — pairs with `_resetSubsetHandlers`. */
|
|
278
|
+
export function _resetDefaultRegistration() { registered = false; }
|
|
279
|
+
//# sourceMappingURL=mandate-handlers.js.map
|