kxco-pq-agent 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ASSESSMENT.md +117 -0
- package/CHANGELOG.md +41 -0
- package/README.md +78 -4
- package/package.json +6 -3
- package/src/index.d.ts +24 -0
- package/src/index.js +1 -1
- package/src/scope.js +121 -0
package/ASSESSMENT.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Assessment notes
|
|
2
|
+
|
|
3
|
+
The answers a buyer's readiness assessment asks for: what this package does,
|
|
4
|
+
how it moves when algorithms move, and what it takes to run it.
|
|
5
|
+
|
|
6
|
+
Algorithm conformance belongs to
|
|
7
|
+
[`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum), which
|
|
8
|
+
runs 2,103 NIST ACVP vectors and a cross-implementation interoperability matrix
|
|
9
|
+
and publishes the lot. Cited here, proven there.
|
|
10
|
+
|
|
11
|
+
## What this package is
|
|
12
|
+
|
|
13
|
+
Identity for a non-human actor. A KYC-verified institution sponsors an ML-DSA-65
|
|
14
|
+
keypair for an agent and binds a capability scope to it at issuance.
|
|
15
|
+
|
|
16
|
+
The question a supervisor asks about an autonomous system is not "was it
|
|
17
|
+
encrypted" but "who authorised this, and what were they allowed to do". This
|
|
18
|
+
package is built to answer exactly that, and three properties hold:
|
|
19
|
+
|
|
20
|
+
**An agent cannot widen its own authority.** The scope is signed by the sponsor
|
|
21
|
+
at issuance and hashed on chain, so it cannot be edited afterwards. Widening
|
|
22
|
+
requires revoke and re-issue by the sponsor. Compromising the agent does not
|
|
23
|
+
enlarge what the agent may do, which is the property that makes an autonomous
|
|
24
|
+
key safe to deploy at all.
|
|
25
|
+
|
|
26
|
+
**An agent cannot mint another agent.** Only a KYC-verified sponsor issues an
|
|
27
|
+
identity. There is no path from one compromised agent to a population of them.
|
|
28
|
+
|
|
29
|
+
**Every agent traces to a named legal entity.** No anonymous mode, no
|
|
30
|
+
self-signed mode. When a regulator asks who authorised an action, the answer is
|
|
31
|
+
an institution that completed KYC, not a key of unknown provenance.
|
|
32
|
+
|
|
33
|
+
**Enforcement runs at both ends.** The relay checks scope behind the agent,
|
|
34
|
+
where a compromised agent cannot reach it, and `checkScope()` checks the same
|
|
35
|
+
signed scope in front:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const decision = checkScope(agent.scope, {
|
|
39
|
+
type: 'payment', amount: 4500, spentToday: 12000, recipient: '0xAbC…',
|
|
40
|
+
})
|
|
41
|
+
// { allowed: false, reason: 'amount 4500 would take today's total to 16500,
|
|
42
|
+
// past maxPerDay 15000', checked: ['payments.enabled', …] }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It refuses offline, so an agent that cannot reach the relay still knows what it
|
|
46
|
+
may not do. It refuses without spending a round trip. And it names the limit that
|
|
47
|
+
stopped it, which a remote refusal cannot do as precisely. It fails closed
|
|
48
|
+
throughout: a capability the scope does not grant is denied, an action type it
|
|
49
|
+
does not recognise is denied, and a configured limit that cannot be judged from
|
|
50
|
+
the inputs given is denied rather than skipped. `checked` reports every limit
|
|
51
|
+
the decision actually evaluated, so a caller can see the control was applied
|
|
52
|
+
rather than assume it.
|
|
53
|
+
|
|
54
|
+
**Expiry is mandatory.** `expiresIn` is required at creation. An agent identity
|
|
55
|
+
cannot be issued without an end date, which is the default that stops a
|
|
56
|
+
short-lived task leaving a long-lived key behind.
|
|
57
|
+
|
|
58
|
+
## Scope
|
|
59
|
+
|
|
60
|
+
The relay's enforcement is the one that binds, because it sits where the agent
|
|
61
|
+
cannot influence it, and `checkScope()` is defence in depth in front of it.
|
|
62
|
+
Assess both: the local check for fast, offline, precise refusal, and the relay
|
|
63
|
+
for the guarantee.
|
|
64
|
+
|
|
65
|
+
The sponsor's KYC is an operational control at KXCO rather than a protocol one,
|
|
66
|
+
and it is the thing that makes attribution to a legal entity meaningful.
|
|
67
|
+
|
|
68
|
+
Actions land where they land: on Armature L1 through
|
|
69
|
+
[`kxco-pq-chain`](https://www.npmjs.com/package/kxco-pq-chain), or in whatever
|
|
70
|
+
record the sponsor keeps, for which
|
|
71
|
+
[`kxco-pq-audit`](https://www.npmjs.com/package/kxco-pq-audit) is the
|
|
72
|
+
append-only option.
|
|
73
|
+
|
|
74
|
+
## Agility
|
|
75
|
+
|
|
76
|
+
**Inherited.** Signing primitives belong to `kxco-post-quantum`.
|
|
77
|
+
|
|
78
|
+
**Coordinated by design.** An agent identity is registered on chain and its
|
|
79
|
+
scope is checked by the relay, so a parameter-set change moves through the chain
|
|
80
|
+
and the relay before it reaches the client. That ordering is correct: an
|
|
81
|
+
identity nobody can verify is worse than one that waits, and it is why the
|
|
82
|
+
migration story for this package is the chain's rather than its own.
|
|
83
|
+
|
|
84
|
+
**The scope manifest is a signed data structure** with named capability
|
|
85
|
+
sections, so adding a capability class is a change the sponsor signs and the
|
|
86
|
+
enforcer recognises, agreed between the two rather than assumed by either.
|
|
87
|
+
|
|
88
|
+
## Running it
|
|
89
|
+
|
|
90
|
+
**Release integrity.** Every release carries a SLSA provenance attestation and
|
|
91
|
+
a CycloneDX SBOM at a permanent unauthenticated URL, plus an evidence bundle
|
|
92
|
+
from `npm run evidence` recording identity, the test run, the SBOM and the
|
|
93
|
+
`kxco-post-quantum` version actually installed rather than the range declared.
|
|
94
|
+
|
|
95
|
+
**Supported versions.** One line moving forward. Fixes land in the next release.
|
|
96
|
+
|
|
97
|
+
**Cost.** No hardware or runtime ceiling. Signing is one ML-DSA-65 operation per
|
|
98
|
+
action; the practical limits are the scope caps themselves, which are policy
|
|
99
|
+
rather than performance.
|
|
100
|
+
|
|
101
|
+
**Connection.** `relay.kxco.ai`, which negotiates the hybrid key exchange group
|
|
102
|
+
`X25519MLKEM768` under TLS 1.3. Measured 7 September 2026 with OpenSSL 3.5.6,
|
|
103
|
+
and reproducible:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
echo | openssl s_client -connect relay.kxco.ai:443 -servername relay.kxco.ai \
|
|
107
|
+
-groups X25519MLKEM768 -tls1_3 2>&1 | grep "Negotiated TLS1.3 group"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The intent is signed with ML-DSA-65 before it is sent and verified on chain
|
|
111
|
+
after it arrives, so the transport carries the intent rather than securing it.
|
|
112
|
+
|
|
113
|
+
## Correcting this document
|
|
114
|
+
|
|
115
|
+
Every claim here is checkable against `src/` and the README. The TLS measurement
|
|
116
|
+
is reproducible with the command given. If one does not match, that is a defect
|
|
117
|
+
worth reporting through the repository's issues.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
**`checkScope()` enforces the scope locally, before the relay sees the action.**
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { checkScope } from 'kxco-pq-agent'
|
|
9
|
+
|
|
10
|
+
const decision = checkScope(agent.scope, {
|
|
11
|
+
type: 'payment', amount: 4500, spentToday: 12000, recipient: '0xAbC…',
|
|
12
|
+
})
|
|
13
|
+
// { allowed: false,
|
|
14
|
+
// reason: "amount 4500 would take today's total to 16500, past maxPerDay 15000",
|
|
15
|
+
// checked: ['payments.enabled', 'payments.maxPerTransaction', 'payments.maxPerDay'] }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The relay's enforcement is unchanged and is still the one that binds, because it
|
|
19
|
+
sits behind the agent where a compromised agent cannot reach it. This runs in
|
|
20
|
+
front, on the same signed scope, and earns its place three ways: it refuses
|
|
21
|
+
offline, so an agent that cannot reach the relay still knows what it may not do;
|
|
22
|
+
it refuses without spending a round trip; and it names the limit that stopped
|
|
23
|
+
it, which a remote refusal cannot do as precisely.
|
|
24
|
+
|
|
25
|
+
**Fails closed throughout.** A capability the scope does not grant is denied. An
|
|
26
|
+
action type it does not recognise is denied. A configured limit that cannot be
|
|
27
|
+
judged from the inputs given is denied rather than skipped — pass a scope with
|
|
28
|
+
`maxPerDay` set and no `spentToday`, and the answer is a refusal that says which
|
|
29
|
+
input is missing, not a pass that quietly skipped a control.
|
|
30
|
+
|
|
31
|
+
`checked` reports every limit the decision actually evaluated, in the order
|
|
32
|
+
applied, so a caller can show the control ran rather than assert it did.
|
|
33
|
+
|
|
34
|
+
Covers payments (`maxPerTransaction`, `maxPerDay`, `allowedRecipients`, with EVM
|
|
35
|
+
recipients matched case-insensitively), attestation purposes, `auditLog` and
|
|
36
|
+
`credentials`. Eleven tests.
|
|
37
|
+
|
|
38
|
+
**ASSESSMENT.md rewritten** to lead with the containment properties — an agent
|
|
39
|
+
cannot widen its own authority, cannot mint another agent, and always traces to
|
|
40
|
+
a KYC-verified institution — and to record that enforcement now runs at both
|
|
41
|
+
ends.
|
package/README.md
CHANGED
|
@@ -4,6 +4,33 @@ Post-quantum identity for AI agents and autonomous systems.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Release integrity
|
|
8
|
+
|
|
9
|
+
Every release of this package is checkable without asking us for anything.
|
|
10
|
+
|
|
11
|
+
- **Provenance.** Each release carries a SLSA provenance attestation tying the
|
|
12
|
+
published tarball to the commit and workflow that built it. Verify with
|
|
13
|
+
`npm audit signatures`, or read it directly from
|
|
14
|
+
`registry.npmjs.org/-/npm/v1/attestations/kxco-pq-agent@<version>`.
|
|
15
|
+
- **Bill of materials.** A CycloneDX SBOM is published as a GitHub Release asset
|
|
16
|
+
at `releases/download/v<version>/sbom.cyclonedx.json`, a permanent
|
|
17
|
+
unauthenticated URL. Not an expiring build artifact.
|
|
18
|
+
- **Pinned where it matters.** Third-party dependencies are pinned to exact
|
|
19
|
+
versions, never ranges, so the code that performs the cryptography cannot
|
|
20
|
+
change without a release. Sibling `kxco-*` packages sit on caret ranges
|
|
21
|
+
deliberately: it means a correctness fix in the base package reaches you
|
|
22
|
+
without a release of every package above it. That is not theoretical. When
|
|
23
|
+
`@noble/post-quantum` 0.7.1 was found to fail NIST SLH-DSA verification
|
|
24
|
+
vectors, the revert in the base package propagated here on the next install.
|
|
25
|
+
Every GitHub Action is pinned by 40-character commit SHA.
|
|
26
|
+
- **Conformance underneath.** The cryptography comes from
|
|
27
|
+
[`kxco-post-quantum`](https://www.npmjs.com/package/kxco-post-quantum), which
|
|
28
|
+
is run against **2,103 NIST ACVP vectors: 1,793 passed, 0 failed, 310 skipped** and a **225-check
|
|
29
|
+
cross-implementation interoperability matrix** against liboqs, Bouncy Castle
|
|
30
|
+
and two pure-Python implementations, in both directions and with negative
|
|
31
|
+
controls. Its published tarball also rebuilds bit-for-bit from its own tag,
|
|
32
|
+
verified in CI on every run.
|
|
33
|
+
|
|
7
34
|
## The problem it solves
|
|
8
35
|
|
|
9
36
|
AI systems cannot pass KYC. An LLM, robot, IoT device, or daemon has no legal standing to authenticate itself to a regulated network. This package solves that with a delegation model: a KYC-verified institution sponsors the agent by signing its ML-DSA-65 public key alongside a locked capability scope. The agent then signs its own relay operations independently, presenting the sponsor's credential as proof of authority. The KXCO relay validates both signatures before accepting any intent — the institution's approval is cryptographically bound to every action the agent takes.
|
|
@@ -149,6 +176,41 @@ Anchors an audit log checkpoint on-chain. Requires `scope.auditLog: true`.
|
|
|
149
176
|
|
|
150
177
|
Submits a payment intent. `to` is an EVM address or KXCO kid. `amount` is in ARMR. The relay enforces `allowedRecipients`, `maxPerTransaction`, and `maxPerDay` from the scope.
|
|
151
178
|
|
|
179
|
+
### `checkScope(scope, action)`
|
|
180
|
+
|
|
181
|
+
Decide whether a scope permits an action, before attempting it.
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
import { checkScope } from 'kxco-pq-agent'
|
|
185
|
+
|
|
186
|
+
checkScope(agent.scope, {
|
|
187
|
+
type: 'payment', amount: 4500, spentToday: 12000, recipient: '0xAbC…',
|
|
188
|
+
})
|
|
189
|
+
// { allowed: false,
|
|
190
|
+
// reason: "amount 4500 would take today's total to 16500, past maxPerDay 15000",
|
|
191
|
+
// checked: ['payments.enabled', 'payments.maxPerTransaction', 'payments.maxPerDay'] }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The relay enforces the same signed scope behind the agent, and that enforcement
|
|
195
|
+
is the one that binds. This runs in front of it: it refuses offline, refuses
|
|
196
|
+
without spending a round trip, and names the limit that stopped it.
|
|
197
|
+
|
|
198
|
+
| Action | Fields | Checked against |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `payment` | `amount`, `recipient`, `spentToday` | `maxPerTransaction`, `maxPerDay`, `allowedRecipients` |
|
|
201
|
+
| `attestation` | `purpose` | `attestations.purposes` |
|
|
202
|
+
| `auditLog` | — | `auditLog` |
|
|
203
|
+
| `credentials` | — | `credentials` |
|
|
204
|
+
|
|
205
|
+
Returns `{ allowed, reason?, checked }`. `checked` lists every limit the
|
|
206
|
+
decision actually evaluated, so a caller can show the control ran.
|
|
207
|
+
|
|
208
|
+
It fails closed: a capability the scope does not grant is denied, an action type
|
|
209
|
+
it does not recognise is denied, and a configured limit that cannot be judged
|
|
210
|
+
from the inputs given is denied rather than skipped. Set `maxPerDay` and omit
|
|
211
|
+
`spentToday` and the answer is a refusal naming the missing input, never a pass
|
|
212
|
+
that skipped the cap.
|
|
213
|
+
|
|
152
214
|
---
|
|
153
215
|
|
|
154
216
|
## Agent types
|
|
@@ -162,11 +224,23 @@ Submits a payment intent. `to` is an EVM address or KXCO kid. `amount` is in ARM
|
|
|
162
224
|
|
|
163
225
|
---
|
|
164
226
|
|
|
165
|
-
##
|
|
227
|
+
## The containment model
|
|
228
|
+
|
|
229
|
+
Three properties hold for every agent identity, and they are the reason to use
|
|
230
|
+
this rather than handing an agent a key.
|
|
231
|
+
|
|
232
|
+
**An agent can never widen its own authority.** The capability scope is fixed
|
|
233
|
+
at issuance and enforced relay-side, so an out-of-scope operation is refused
|
|
234
|
+
before it reaches the chain. Compromising the agent does not enlarge what the
|
|
235
|
+
agent may do.
|
|
236
|
+
|
|
237
|
+
**An agent can never mint another agent.** Only a KYC-verified sponsor issues
|
|
238
|
+
an identity, so there is no path from one compromised agent to a population of
|
|
239
|
+
them.
|
|
166
240
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
241
|
+
**Every agent traces to a named, KYC-verified institution.** There is no
|
|
242
|
+
anonymous or self-signed mode, which is what makes an agent's action
|
|
243
|
+
attributable to a legal entity when a supervisor asks who authorised it.
|
|
170
244
|
|
|
171
245
|
---
|
|
172
246
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kxco-pq-agent",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Post-quantum identity for AI agents and autonomous systems: a KYC-verified institution sponsors an ML-DSA-65 keypair and locked capability scope for any agent that cannot pass KYC itself.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"post-quantum",
|
|
@@ -47,8 +47,10 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"files": [
|
|
50
|
+
"CHANGELOG.md",
|
|
50
51
|
"src",
|
|
51
|
-
"LICENSE"
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"ASSESSMENT.md"
|
|
52
54
|
],
|
|
53
55
|
"engines": {
|
|
54
56
|
"node": ">=20.19"
|
|
@@ -57,7 +59,8 @@
|
|
|
57
59
|
"kxco-post-quantum": "^1.3.0"
|
|
58
60
|
},
|
|
59
61
|
"scripts": {
|
|
60
|
-
"test": "node --test --test-timeout=30000 test/agent.test.js"
|
|
62
|
+
"test": "node --test --test-timeout=30000 test/agent.test.js",
|
|
63
|
+
"evidence": "node scripts/build-evidence.mjs"
|
|
61
64
|
},
|
|
62
65
|
"funding": "https://kxco.ai",
|
|
63
66
|
"publishConfig": {
|
package/src/index.d.ts
CHANGED
|
@@ -38,6 +38,30 @@ export interface AgentScope {
|
|
|
38
38
|
export function validateScope(scope: AgentScope): AgentScope
|
|
39
39
|
export function hashScope(scope: AgentScope): Promise<string>
|
|
40
40
|
|
|
41
|
+
export type ScopeAction =
|
|
42
|
+
| { type: 'payment'; amount: number; recipient?: string; spentToday?: number }
|
|
43
|
+
| { type: 'attestation'; purpose?: string }
|
|
44
|
+
| { type: 'auditLog' }
|
|
45
|
+
| { type: 'credentials' }
|
|
46
|
+
|
|
47
|
+
export interface ScopeDecision {
|
|
48
|
+
allowed: boolean
|
|
49
|
+
/** Why the action was refused. Absent when allowed. */
|
|
50
|
+
reason?: string
|
|
51
|
+
/** Every limit the decision actually evaluated, in the order applied. */
|
|
52
|
+
checked: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decide whether a scope permits an action, before it is attempted.
|
|
57
|
+
*
|
|
58
|
+
* Runs in front of the relay's own enforcement, on the same signed scope: it
|
|
59
|
+
* refuses offline, refuses without spending a round trip, and names the limit
|
|
60
|
+
* that stopped it. Fails closed on an ungranted capability, an unknown action
|
|
61
|
+
* type, and a configured limit that cannot be evaluated from the inputs given.
|
|
62
|
+
*/
|
|
63
|
+
export function checkScope(scope: AgentScope, action: ScopeAction): ScopeDecision
|
|
64
|
+
|
|
41
65
|
// ── Credential envelope ───────────────────────────────────────────────────────
|
|
42
66
|
|
|
43
67
|
export interface AgentCredential {
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { KxcoAgentIdentity } from './agent-identity.js'
|
|
2
2
|
export { AgentChainClient } from './agent-client.js'
|
|
3
3
|
export { KxcoPqAgentError } from './errors.js'
|
|
4
|
-
export { validateScope, hashScope } from './scope.js'
|
|
4
|
+
export { validateScope, checkScope, hashScope } from './scope.js'
|
package/src/scope.js
CHANGED
|
@@ -81,6 +81,127 @@ export function validateScope(scope) {
|
|
|
81
81
|
return scope
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Decide whether a scope permits an action, before it is attempted.
|
|
86
|
+
*
|
|
87
|
+
* The relay enforces scope too, and that enforcement is the one that binds: it
|
|
88
|
+
* sits behind the agent, so a compromised agent cannot talk its way past it.
|
|
89
|
+
* This check runs in front, on the same signed scope, and it earns its place
|
|
90
|
+
* three ways. It refuses offline, so an agent that cannot reach the relay still
|
|
91
|
+
* knows what it may not do. It refuses immediately, without spending a network
|
|
92
|
+
* round trip to be told no. And it names the limit that stopped it, which a
|
|
93
|
+
* remote refusal cannot do as precisely.
|
|
94
|
+
*
|
|
95
|
+
* Fails closed throughout. A capability the scope does not grant is denied, an
|
|
96
|
+
* action type it does not recognise is denied, and a configured limit that
|
|
97
|
+
* cannot be evaluated from the inputs given is denied rather than skipped.
|
|
98
|
+
*
|
|
99
|
+
* @param {object} scope — the signed capability manifest
|
|
100
|
+
* @param {object} action
|
|
101
|
+
* @param {'payment'|'attestation'|'auditLog'|'credentials'} action.type
|
|
102
|
+
* @param {number} [action.amount] — payment only, ARMR
|
|
103
|
+
* @param {string} [action.recipient] — payment only, EVM address or kid
|
|
104
|
+
* @param {number} [action.spentToday] — payment only, required when
|
|
105
|
+
* `payments.maxPerDay` is set, because a day cap cannot be judged from one
|
|
106
|
+
* transaction
|
|
107
|
+
* @param {string} [action.purpose] — attestation only
|
|
108
|
+
* @returns {{ allowed: boolean, reason?: string, checked: string[] }}
|
|
109
|
+
*/
|
|
110
|
+
export function checkScope(scope, action) {
|
|
111
|
+
if (!scope || typeof scope !== 'object' || Array.isArray(scope)) {
|
|
112
|
+
throw new KxcoPqAgentError('checkScope: scope must be a plain object')
|
|
113
|
+
}
|
|
114
|
+
if (!action || typeof action !== 'object' || Array.isArray(action)) {
|
|
115
|
+
throw new KxcoPqAgentError('checkScope: action must be a plain object')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const checked = []
|
|
119
|
+
const deny = (reason) => ({ allowed: false, reason, checked })
|
|
120
|
+
const allow = () => ({ allowed: true, checked })
|
|
121
|
+
|
|
122
|
+
const granted = (section) =>
|
|
123
|
+
section != null && section !== false && section.enabled !== false
|
|
124
|
+
|
|
125
|
+
switch (action.type) {
|
|
126
|
+
case 'payment': {
|
|
127
|
+
const p = scope.payments
|
|
128
|
+
checked.push('payments.enabled')
|
|
129
|
+
if (!granted(p)) return deny('scope does not grant payments')
|
|
130
|
+
|
|
131
|
+
if (typeof action.amount !== 'number' || !(action.amount > 0)) {
|
|
132
|
+
return deny('payment amount must be a positive number')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (p.maxPerTransaction !== undefined) {
|
|
136
|
+
checked.push('payments.maxPerTransaction')
|
|
137
|
+
if (action.amount > p.maxPerTransaction) {
|
|
138
|
+
return deny(
|
|
139
|
+
`amount ${action.amount} exceeds maxPerTransaction ${p.maxPerTransaction}`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (p.maxPerDay !== undefined) {
|
|
145
|
+
checked.push('payments.maxPerDay')
|
|
146
|
+
if (typeof action.spentToday !== 'number' || action.spentToday < 0) {
|
|
147
|
+
return deny(
|
|
148
|
+
'payments.maxPerDay is set, so spentToday is required to evaluate it',
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
if (action.spentToday + action.amount > p.maxPerDay) {
|
|
152
|
+
return deny(
|
|
153
|
+
`amount ${action.amount} would take today's total to ` +
|
|
154
|
+
`${action.spentToday + action.amount}, past maxPerDay ${p.maxPerDay}`,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (p.allowedRecipients !== undefined) {
|
|
160
|
+
checked.push('payments.allowedRecipients')
|
|
161
|
+
if (typeof action.recipient !== 'string') {
|
|
162
|
+
return deny('payments.allowedRecipients is set, so a recipient is required')
|
|
163
|
+
}
|
|
164
|
+
// EVM addresses are case-insensitive; kids are lowercase hex.
|
|
165
|
+
const want = action.recipient.toLowerCase()
|
|
166
|
+
const ok = p.allowedRecipients.some((r) => r.toLowerCase() === want)
|
|
167
|
+
if (!ok) return deny(`recipient ${action.recipient} is not in allowedRecipients`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return allow()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'attestation': {
|
|
174
|
+
const a = scope.attestations
|
|
175
|
+
checked.push('attestations.enabled')
|
|
176
|
+
if (!granted(a)) return deny('scope does not grant attestations')
|
|
177
|
+
|
|
178
|
+
if (a.purposes !== undefined) {
|
|
179
|
+
checked.push('attestations.purposes')
|
|
180
|
+
if (typeof action.purpose !== 'string') {
|
|
181
|
+
return deny('attestations.purposes is set, so a purpose is required')
|
|
182
|
+
}
|
|
183
|
+
if (!a.purposes.includes(action.purpose)) {
|
|
184
|
+
return deny(`purpose '${action.purpose}' is not in attestations.purposes`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return allow()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case 'auditLog':
|
|
191
|
+
checked.push('auditLog')
|
|
192
|
+
return granted(scope.auditLog) ? allow() : deny('scope does not grant auditLog')
|
|
193
|
+
|
|
194
|
+
case 'credentials':
|
|
195
|
+
checked.push('credentials')
|
|
196
|
+
return granted(scope.credentials)
|
|
197
|
+
? allow()
|
|
198
|
+
: deny('scope does not grant credentials')
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
return deny(`unknown action type '${action.type}'`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
84
205
|
/**
|
|
85
206
|
* Compute a hex SHA-256 of the JCS-canonical scope.
|
|
86
207
|
* This hash is stored on-chain so the relay can verify scope integrity.
|