@mandatum/sdk 1.0.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/README.md +212 -0
- package/index.js +318 -0
- package/local-verifier.js +102 -0
- package/package.json +48 -0
- package/payments/razorpay.js +118 -0
- package/payments/stripe.js +109 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# @mandatum/sdk
|
|
2
|
+
|
|
3
|
+
> **Zero-trust authorization and spending limits for AI agents.** One line of code to verify permissions, enforce budgets, and generate tamper-proof audit receipts.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@mandatum/sdk)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @mandatum/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
import { Mandatum } from '@mandatum/sdk';
|
|
18
|
+
|
|
19
|
+
const mandatum = new Mandatum({
|
|
20
|
+
apiKey: 'mdt_live_xxxxxxxxxxxxxxxx',
|
|
21
|
+
baseUrl: 'https://mandatum-fbg9.onrender.com', // or your self-hosted URL
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ─── 1. Register an Agent ───────────────────────────────
|
|
25
|
+
const agent = await mandatum.agents.create({
|
|
26
|
+
name: 'PaymentBot',
|
|
27
|
+
type: 'payment',
|
|
28
|
+
delegator: 'cfo@yourcompany.com',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ─── 2. Create a Spending Policy ────────────────────────
|
|
32
|
+
const policy = await mandatum.policies.create({
|
|
33
|
+
name: 'cloud-budget',
|
|
34
|
+
resource: 'payments',
|
|
35
|
+
actions: ['payments:execute'],
|
|
36
|
+
constraints: {
|
|
37
|
+
max_amount: 5000,
|
|
38
|
+
daily_limit: 10000,
|
|
39
|
+
currency: 'INR',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
await mandatum.policies.assign(agent.id, policy.id);
|
|
43
|
+
|
|
44
|
+
// ─── 3. Issue a Scoped Token ────────────────────────────
|
|
45
|
+
const { token } = await mandatum.tokens.issue({
|
|
46
|
+
agentId: agent.id,
|
|
47
|
+
scopes: ['payments:execute'],
|
|
48
|
+
constraints: { max_amount: 5000, currency: 'INR' },
|
|
49
|
+
expiresIn: '1h',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ─── 4. The 1-Line Permission Gate ─────────────────────
|
|
53
|
+
const result = await mandatum.check(token, 'payments:execute', {
|
|
54
|
+
amount: 350,
|
|
55
|
+
currency: 'INR',
|
|
56
|
+
vendor: 'DigitalOcean',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.allowed) {
|
|
60
|
+
console.log('✅ Approved — receipt:', result.receipt.receiptId);
|
|
61
|
+
// Proceed with payment
|
|
62
|
+
} else {
|
|
63
|
+
console.log('❌ Blocked:', result.reason);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Core Concepts
|
|
68
|
+
|
|
69
|
+
### The Permission Gate
|
|
70
|
+
|
|
71
|
+
Every agent action goes through `mandatum.check()`. It verifies the JWT signature (Ed25519), checks scopes, evaluates spending constraints, enforces rate limits, and returns a cryptographically signed receipt — all in one call.
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const result = await mandatum.check(token, action, context);
|
|
75
|
+
// result.allowed → boolean
|
|
76
|
+
// result.reason → why it was blocked (if denied)
|
|
77
|
+
// result.receipt → { receiptId, signature, timestamp }
|
|
78
|
+
// result.checks → detailed constraint evaluations
|
|
79
|
+
// result.rateLimit → current usage stats
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Quick Boolean Check
|
|
83
|
+
|
|
84
|
+
If you just need a true/false gate:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
if (await mandatum.isAllowed(token, 'email:send', { to: 'user@example.com' })) {
|
|
88
|
+
sendEmail();
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API Reference
|
|
93
|
+
|
|
94
|
+
### `new Mandatum(config)`
|
|
95
|
+
|
|
96
|
+
| Option | Type | Default | Description |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `apiKey` | `string` | *required* | Your API key (`mdt_live_...`) |
|
|
99
|
+
| `baseUrl` | `string` | `http://localhost:3001` | Mandatum API URL |
|
|
100
|
+
| `timeout` | `number` | `10000` | Request timeout in ms |
|
|
101
|
+
|
|
102
|
+
### Agents
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
mandatum.agents.create({ name, type, delegator, description })
|
|
106
|
+
mandatum.agents.list({ status, search, environment })
|
|
107
|
+
mandatum.agents.get(id)
|
|
108
|
+
mandatum.agents.suspend(id)
|
|
109
|
+
mandatum.agents.revoke(id) // cascading — revokes all child agents + tokens
|
|
110
|
+
mandatum.agents.spend(id, { amount, currency, vendor, category })
|
|
111
|
+
mandatum.agents.spending(id) // returns today/total spent + remaining budget
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Tokens
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
mandatum.tokens.issue({ agentId, scopes, constraints, expiresIn })
|
|
118
|
+
mandatum.tokens.verify(token, action, context)
|
|
119
|
+
mandatum.tokens.revoke(jti)
|
|
120
|
+
mandatum.tokens.list({ agentId, status })
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Policies
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
mandatum.policies.create({ name, resource, actions, constraints })
|
|
127
|
+
mandatum.policies.list()
|
|
128
|
+
mandatum.policies.get(id)
|
|
129
|
+
mandatum.policies.assign(agentId, policyId)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Audit
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
mandatum.audit.list({ limit, agentId })
|
|
136
|
+
mandatum.audit.verifyChain() // verify hash-chain integrity
|
|
137
|
+
mandatum.audit.getReceipt(eventId) // get signed receipt for an event
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Payment Gateway Wrappers
|
|
141
|
+
|
|
142
|
+
Pre-built wrappers that call `mandatum.check()` before creating payments:
|
|
143
|
+
|
|
144
|
+
### Razorpay
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import { MandatumRazorpay } from '@mandatum/sdk/payments/razorpay';
|
|
148
|
+
|
|
149
|
+
const pay = new MandatumRazorpay(razorpayClient, mandatum, agent.id);
|
|
150
|
+
|
|
151
|
+
const result = await pay.createPayment({
|
|
152
|
+
amount: 50000, // paise
|
|
153
|
+
currency: 'INR',
|
|
154
|
+
description: 'Cloud hosting',
|
|
155
|
+
});
|
|
156
|
+
// Automatically checks limits → creates Razorpay order → records spending
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Stripe
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
import { MandatumStripe } from '@mandatum/sdk/payments/stripe';
|
|
163
|
+
|
|
164
|
+
const pay = new MandatumStripe(stripeClient, mandatum, agent.id);
|
|
165
|
+
|
|
166
|
+
const result = await pay.createPaymentIntent({
|
|
167
|
+
amount: 5000, // cents
|
|
168
|
+
currency: 'usd',
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Local Verification (Sub-1ms)
|
|
173
|
+
|
|
174
|
+
For latency-critical paths, verify token signatures locally without a network round-trip:
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
const mandatum = new Mandatum({
|
|
178
|
+
apiKey: 'mdt_live_...',
|
|
179
|
+
localVerification: true, // fetches public key on init
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// check() now verifies the signature locally first,
|
|
183
|
+
// then calls the server only for constraint evaluation
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Error Handling
|
|
187
|
+
|
|
188
|
+
All errors throw `MandatumError` with a `code` property:
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
import { MandatumError } from '@mandatum/sdk';
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await mandatum.check(token, 'payments:execute', { amount: 999999 });
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err instanceof MandatumError) {
|
|
197
|
+
console.log(err.code); // 'RATE_LIMIT_MINUTE', 'TIMEOUT', 'API_ERROR'
|
|
198
|
+
console.log(err.status); // HTTP status code (if applicable)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Security
|
|
204
|
+
|
|
205
|
+
- All tokens are **Ed25519-signed JWTs** — tamper-proof and verifiable with the public key
|
|
206
|
+
- API keys are **SHA-256 hashed** before storage — Mandatum never stores raw keys
|
|
207
|
+
- Audit events are **hash-chained** — any tampering breaks the chain
|
|
208
|
+
- Receipts are **cryptographically signed** — non-repudiable proof of authorization
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT © [Mandatum](https://mandatumio.vercel.app)
|
package/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mandatum/sdk — Mandatum SDK for Agent Platforms
|
|
3
|
+
*
|
|
4
|
+
* Install: npm install @mandatum/sdk
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { Mandatum } from '@mandatum/sdk';
|
|
8
|
+
* const m = new Mandatum({ apiKey: 'mdt_live_...' });
|
|
9
|
+
*
|
|
10
|
+
* // The 1-line permission gate
|
|
11
|
+
* const ok = await m.check(token, 'payments:execute', { amount: 350 });
|
|
12
|
+
* if (ok.allowed) { ... }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class MandatumError extends Error {
|
|
16
|
+
constructor(message, code, status) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'MandatumError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.status = status;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Mandatum {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} config
|
|
27
|
+
* @param {string} config.apiKey - Mandatum API key (mdt_live_...)
|
|
28
|
+
* @param {string} [config.baseUrl] - API base URL (default: https://api.mandatum.io)
|
|
29
|
+
* @param {number} [config.timeout] - Request timeout in ms (default: 10000)
|
|
30
|
+
* @param {boolean} [config.localVerification] - Enable local token verification (default: false)
|
|
31
|
+
*/
|
|
32
|
+
constructor({ apiKey, baseUrl, timeout = 10000, localVerification = false } = {}) {
|
|
33
|
+
if (!apiKey) throw new MandatumError('API key is required', 'MISSING_API_KEY');
|
|
34
|
+
this.apiKey = apiKey;
|
|
35
|
+
this.baseUrl = (baseUrl || 'http://localhost:3001').replace(/\/$/, '');
|
|
36
|
+
this.timeout = timeout;
|
|
37
|
+
|
|
38
|
+
// Sub-modules
|
|
39
|
+
this.agents = new AgentsAPI(this);
|
|
40
|
+
this.tokens = new TokensAPI(this);
|
|
41
|
+
this.policies = new PoliciesAPI(this);
|
|
42
|
+
this.audit = new AuditAPI(this);
|
|
43
|
+
|
|
44
|
+
// Local verification — store as ready promise (no async in constructor)
|
|
45
|
+
this._localVerifierReady = localVerification
|
|
46
|
+
? this._initLocalVerifier()
|
|
47
|
+
: Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ─── THE 1-LINE PERMISSION GATE ───────────────────────
|
|
52
|
+
*
|
|
53
|
+
* Call this before every agent action.
|
|
54
|
+
* Returns { allowed, reason, receipt, checks }
|
|
55
|
+
*
|
|
56
|
+
* @param {string} token - The agent's JWT token
|
|
57
|
+
* @param {string} action - What the agent wants to do (e.g., 'payments:execute')
|
|
58
|
+
* @param {object} [context] - Action context (amount, currency, vendor, etc.)
|
|
59
|
+
* @returns {Promise<object>}
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const result = await mandatum.check(agentToken, 'payments:execute', {
|
|
63
|
+
* amount: 350,
|
|
64
|
+
* currency: 'INR',
|
|
65
|
+
* vendor: 'DigitalOcean'
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* if (result.allowed) {
|
|
69
|
+
* // Proceed with payment
|
|
70
|
+
* razorpay.pay(350);
|
|
71
|
+
* } else {
|
|
72
|
+
* console.log('Blocked:', result.reason);
|
|
73
|
+
* }
|
|
74
|
+
*/
|
|
75
|
+
async check(token, action, context = {}) {
|
|
76
|
+
// Use local verification if available (sub-1ms)
|
|
77
|
+
if (this._localVerifier) {
|
|
78
|
+
const local = await this._localVerifier.verify(token);
|
|
79
|
+
if (!local.valid) {
|
|
80
|
+
return { allowed: false, valid: false, reason: local.error, code: local.code };
|
|
81
|
+
}
|
|
82
|
+
// Local can't check constraints fully — call server for that
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await this._request('POST', '/tokens/verify', { token, action, context });
|
|
86
|
+
const CHECK_DEFAULTS = {
|
|
87
|
+
allowed: false, valid: false, agent: undefined, delegator: undefined,
|
|
88
|
+
reason: null, receipt: null, checks: [], scopes: [],
|
|
89
|
+
constraints: {}, expiresAt: undefined, error: null, code: null,
|
|
90
|
+
};
|
|
91
|
+
return { ...CHECK_DEFAULTS, ...result };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Quick permission check — returns just true/false
|
|
96
|
+
*/
|
|
97
|
+
async isAllowed(token, action, context = {}) {
|
|
98
|
+
const result = await this.check(token, action, context);
|
|
99
|
+
return result.allowed === true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get server health status
|
|
104
|
+
*/
|
|
105
|
+
async health() {
|
|
106
|
+
return this._request('GET', '/health', null, false);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get server public key (for local verification)
|
|
111
|
+
*/
|
|
112
|
+
async getPublicKey() {
|
|
113
|
+
return this._request('GET', '/.well-known/public-key', null, false);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Internal HTTP Client ─────────────────────────────
|
|
117
|
+
|
|
118
|
+
async _request(method, path, body, authenticated = true) {
|
|
119
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
120
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
121
|
+
|
|
122
|
+
if (authenticated) {
|
|
123
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(url, {
|
|
131
|
+
method,
|
|
132
|
+
headers,
|
|
133
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
134
|
+
signal: controller.signal,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
clearTimeout(timeoutId);
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
throw new MandatumError(
|
|
142
|
+
data.error || data.message || `HTTP ${res.status}`,
|
|
143
|
+
data.code || 'API_ERROR',
|
|
144
|
+
res.status
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return data;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
if (err.name === 'AbortError') {
|
|
152
|
+
throw new MandatumError('Request timed out', 'TIMEOUT');
|
|
153
|
+
}
|
|
154
|
+
if (err instanceof MandatumError) throw err;
|
|
155
|
+
throw new MandatumError(err.message, 'NETWORK_ERROR');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async _initLocalVerifier() {
|
|
160
|
+
try {
|
|
161
|
+
const { LocalVerifier } = await import('./local-verifier.js');
|
|
162
|
+
this._localVerifier = new LocalVerifier(this);
|
|
163
|
+
await this._localVerifier.init();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.warn('Mandatum: Local verification not available, using remote only');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Agents Sub-API ─────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
class AgentsAPI {
|
|
173
|
+
constructor(client) { this.client = client; }
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Register a new AI agent.
|
|
177
|
+
* Generates an Ed25519 key pair on the server.
|
|
178
|
+
*
|
|
179
|
+
* @param {object} params
|
|
180
|
+
* @param {string} params.name - Agent name
|
|
181
|
+
* @param {string} params.type - Agent type (payment, email, calendar, etc.)
|
|
182
|
+
* @param {string} [params.description]
|
|
183
|
+
* @param {string} [params.delegator] - Human who authorized this agent
|
|
184
|
+
*/
|
|
185
|
+
async create({ name, type, description, delegator }) {
|
|
186
|
+
return this.client._request('POST', '/agents', { name, type, description, delegator });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** List all agents */
|
|
190
|
+
async list(params = {}) {
|
|
191
|
+
const qs = new URLSearchParams(params).toString();
|
|
192
|
+
return this.client._request('GET', `/agents${qs ? '?' + qs : ''}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Get agent details (includes keys, policies, tokens, activity) */
|
|
196
|
+
async get(id) {
|
|
197
|
+
return this.client._request('GET', `/agents/${id}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Update agent status (active, suspended, revoked) */
|
|
201
|
+
async update(id, body) {
|
|
202
|
+
return this.client._request('PATCH', `/agents/${id}`, body);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Suspend an agent */
|
|
206
|
+
async suspend(id) { return this.update(id, { status: 'suspended' }); }
|
|
207
|
+
|
|
208
|
+
/** Revoke an agent (and all its tokens) */
|
|
209
|
+
async revoke(id) { return this.update(id, { status: 'revoked' }); }
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Record a spending event — the SDK calls this before/after payment.
|
|
213
|
+
* Returns { allowed, eventId, amount, spending } or { allowed: false, reason }.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} id - Agent ID
|
|
216
|
+
* @param {object} params
|
|
217
|
+
* @param {number} params.amount - Spending amount
|
|
218
|
+
* @param {string} [params.currency] - Currency (default: INR)
|
|
219
|
+
* @param {string} [params.vendor] - Vendor name
|
|
220
|
+
* @param {string} [params.category] - Category (e.g., 'cloud', 'groceries')
|
|
221
|
+
* @param {string} [params.description] - Human-readable description
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* const result = await mandatum.agents.spend('agt_abc123', {
|
|
225
|
+
* amount: 350, currency: 'INR', vendor: 'DigitalOcean'
|
|
226
|
+
* });
|
|
227
|
+
* if (result.allowed) { // proceed with payment }
|
|
228
|
+
*/
|
|
229
|
+
async spend(id, { amount, currency, vendor, category, description } = {}) {
|
|
230
|
+
return this.client._request('POST', `/agents/${id}/spend`, {
|
|
231
|
+
amount, currency, vendor, category, description,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get spending summary for an agent.
|
|
237
|
+
* Returns today/total spent, remaining budget, recent events.
|
|
238
|
+
*/
|
|
239
|
+
async spending(id) {
|
|
240
|
+
return this.client._request('GET', `/agents/${id}/spending`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Tokens Sub-API ─────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
class TokensAPI {
|
|
247
|
+
constructor(client) { this.client = client; }
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Issue a signed JWT token for an agent.
|
|
251
|
+
*
|
|
252
|
+
* @param {object} params
|
|
253
|
+
* @param {string} params.agentId - Agent to issue token for
|
|
254
|
+
* @param {string[]} params.scopes - Allowed actions
|
|
255
|
+
* @param {object} [params.constraints] - Limits (max_amount, currency, etc.)
|
|
256
|
+
* @param {string} [params.expiresIn] - Duration ('15m', '1h', '30d')
|
|
257
|
+
*/
|
|
258
|
+
async issue({ agentId, scopes, constraints, policyId, expiresIn }) {
|
|
259
|
+
return this.client._request('POST', '/tokens/issue', {
|
|
260
|
+
agentId, scopes, constraints, policyId, expiresIn,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Verify a token (signature + constraints + revocation).
|
|
266
|
+
* This is what mandatum.check() calls internally.
|
|
267
|
+
*/
|
|
268
|
+
async verify(token, action, context) {
|
|
269
|
+
return this.client._request('POST', '/tokens/verify', { token, action, context });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Revoke a token immediately */
|
|
273
|
+
async revoke(jti) {
|
|
274
|
+
return this.client._request('POST', '/tokens/revoke', { jti });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** List all tokens */
|
|
278
|
+
async list(params = {}) {
|
|
279
|
+
const qs = new URLSearchParams(params).toString();
|
|
280
|
+
return this.client._request('GET', `/tokens${qs ? '?' + qs : ''}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Policies Sub-API ───────────────────────────────────
|
|
285
|
+
|
|
286
|
+
class PoliciesAPI {
|
|
287
|
+
constructor(client) { this.client = client; }
|
|
288
|
+
|
|
289
|
+
async create({ name, resource, actions, constraints, description }) {
|
|
290
|
+
return this.client._request('POST', '/policies', { name, resource, actions, constraints, description });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async list() { return this.client._request('GET', '/policies'); }
|
|
294
|
+
|
|
295
|
+
async get(id) { return this.client._request('GET', `/policies/${id}`); }
|
|
296
|
+
|
|
297
|
+
async assign(agentId, policyId) {
|
|
298
|
+
return this.client._request('POST', '/policies/assign', { agentId, policyId });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Audit Sub-API ──────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
class AuditAPI {
|
|
305
|
+
constructor(client) { this.client = client; }
|
|
306
|
+
|
|
307
|
+
async list(params = {}) {
|
|
308
|
+
const qs = new URLSearchParams(params).toString();
|
|
309
|
+
return this.client._request('GET', `/audit${qs ? '?' + qs : ''}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async verifyChain() { return this.client._request('GET', '/audit/verify-chain'); }
|
|
313
|
+
|
|
314
|
+
async getReceipt(eventId) { return this.client._request('GET', `/audit/${eventId}/receipt`); }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export default Mandatum;
|
|
318
|
+
export { MandatumError };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Token Verifier
|
|
3
|
+
*
|
|
4
|
+
* Verifies Mandatum JWT tokens locally without a network call.
|
|
5
|
+
* Caches the server's Ed25519 public key and verifies signatures in <1ms.
|
|
6
|
+
*
|
|
7
|
+
* Limitations:
|
|
8
|
+
* - Cannot check revocation locally (needs server for real-time revocation)
|
|
9
|
+
* - Cannot evaluate complex constraints
|
|
10
|
+
* - Best used as a fast pre-check; fall back to server for full verification
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const m = new Mandatum({ apiKey: '...', localVerification: true });
|
|
14
|
+
* // SDK automatically uses local verification when possible
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as jose from 'jose';
|
|
18
|
+
|
|
19
|
+
export class LocalVerifier {
|
|
20
|
+
constructor(client) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.publicKey = null;
|
|
23
|
+
this.publicKeyPem = null;
|
|
24
|
+
this.lastRefresh = 0;
|
|
25
|
+
this.refreshInterval = 5 * 60 * 1000; // Refresh public key every 5 minutes
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize: fetch and cache the server's Ed25519 public key.
|
|
30
|
+
*/
|
|
31
|
+
async init() {
|
|
32
|
+
await this.refreshPublicKey();
|
|
33
|
+
console.log('Mandatum: Local verification initialized (sub-1ms token checks)');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Refresh the cached public key from the server.
|
|
38
|
+
*/
|
|
39
|
+
async refreshPublicKey() {
|
|
40
|
+
try {
|
|
41
|
+
const data = await this.client.getPublicKey();
|
|
42
|
+
this.publicKeyPem = data.publicKey;
|
|
43
|
+
this.publicKey = await jose.importSPKI(data.publicKey, 'EdDSA');
|
|
44
|
+
this.lastRefresh = Date.now();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn('Mandatum: Failed to refresh public key:', err.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Verify a token locally.
|
|
52
|
+
* Checks: signature validity, expiration, issuer.
|
|
53
|
+
* Does NOT check: revocation, complex constraints.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} token - JWT string
|
|
56
|
+
* @returns {Promise<object>} { valid, payload, error }
|
|
57
|
+
*/
|
|
58
|
+
async verify(token) {
|
|
59
|
+
// Refresh key if stale
|
|
60
|
+
if (Date.now() - this.lastRefresh > this.refreshInterval) {
|
|
61
|
+
await this.refreshPublicKey();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!this.publicKey) {
|
|
65
|
+
return { valid: false, error: 'No public key cached', code: 'NO_PUBLIC_KEY' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const { payload } = await jose.jwtVerify(token, this.publicKey, {
|
|
70
|
+
issuer: 'mandatum.io',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
valid: true,
|
|
75
|
+
payload,
|
|
76
|
+
agent: payload.sub,
|
|
77
|
+
scopes: payload.scopes,
|
|
78
|
+
constraints: payload.constraints,
|
|
79
|
+
delegator: payload.delegator,
|
|
80
|
+
expiresAt: new Date(payload.exp * 1000).toISOString(),
|
|
81
|
+
_note: 'Verified locally. Revocation status not checked — use server verify for full check.',
|
|
82
|
+
};
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err.code === 'ERR_JWT_EXPIRED') {
|
|
85
|
+
return { valid: false, error: 'Token expired', code: 'TOKEN_EXPIRED' };
|
|
86
|
+
}
|
|
87
|
+
if (err.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
|
|
88
|
+
return { valid: false, error: 'Invalid signature', code: 'INVALID_SIGNATURE' };
|
|
89
|
+
}
|
|
90
|
+
return { valid: false, error: err.message, code: 'VERIFICATION_FAILED' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Decode a token without verifying (for debugging).
|
|
96
|
+
*/
|
|
97
|
+
decode(token) {
|
|
98
|
+
return jose.decodeJwt(token);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default LocalVerifier;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandatum/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-trust authorization and spending limits for AI agents. Ed25519-signed JWTs, policy enforcement, and payment gating in one line.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./local": "./local-verifier.js",
|
|
10
|
+
"./payments/razorpay": "./payments/razorpay.js",
|
|
11
|
+
"./payments/stripe": "./payments/stripe.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"local-verifier.js",
|
|
16
|
+
"payments/",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"agents",
|
|
22
|
+
"security",
|
|
23
|
+
"authorization",
|
|
24
|
+
"jwt",
|
|
25
|
+
"ed25519",
|
|
26
|
+
"spending-limits",
|
|
27
|
+
"razorpay",
|
|
28
|
+
"stripe",
|
|
29
|
+
"payments",
|
|
30
|
+
"mandatum"
|
|
31
|
+
],
|
|
32
|
+
"author": "Mandatum <hello@mandatum.io>",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"homepage": "https://mandatumio.vercel.app",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/sagevedant/mandatum"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/sagevedant/mandatum/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"jose": "^6.1.3"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandatum Razorpay Payment Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps Razorpay API calls with Mandatum authorization.
|
|
5
|
+
* The agent must get approval from Mandatum before any payment goes through.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import Razorpay from 'razorpay';
|
|
9
|
+
* import { Mandatum } from '@mandatum/sdk';
|
|
10
|
+
* import { MandatumRazorpay } from '@mandatum/sdk/payments/razorpay';
|
|
11
|
+
*
|
|
12
|
+
* const razorpay = new Razorpay({ key_id: '...', key_secret: '...' });
|
|
13
|
+
* const mandatum = new Mandatum({ apiKey: 'mdt_live_...' });
|
|
14
|
+
*
|
|
15
|
+
* const pay = new MandatumRazorpay(razorpay, mandatum, 'agt_abc123');
|
|
16
|
+
* const result = await pay.createPayment({ amount: 50000, currency: 'INR' });
|
|
17
|
+
* // Mandatum checks limits FIRST, then Razorpay creates the payment
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class MandatumRazorpay {
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} razorpayClient - Initialized Razorpay instance
|
|
23
|
+
* @param {object} mandatumClient - Initialized Mandatum SDK instance
|
|
24
|
+
* @param {string} agentId - The agent making the payment
|
|
25
|
+
*/
|
|
26
|
+
constructor(razorpayClient, mandatumClient, agentId) {
|
|
27
|
+
this.razorpay = razorpayClient;
|
|
28
|
+
this.mandatum = mandatumClient;
|
|
29
|
+
this.agentId = agentId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a payment — checks Mandatum limits first.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} params
|
|
36
|
+
* @param {number} params.amount - Amount in smallest currency unit (paise for INR)
|
|
37
|
+
* @param {string} [params.currency='INR'] - Currency
|
|
38
|
+
* @param {string} [params.description] - Payment description
|
|
39
|
+
* @param {object} [params.notes] - Razorpay notes
|
|
40
|
+
* @returns {Promise<{ allowed: boolean, payment?: object, reason?: string, eventId: string }>}
|
|
41
|
+
*/
|
|
42
|
+
async createPayment({ amount, currency = 'INR', description, notes, ...rest }) {
|
|
43
|
+
// Convert paise to rupees for Mandatum (Razorpay uses paise, Mandatum uses rupees)
|
|
44
|
+
const amountInRupees = amount / 100;
|
|
45
|
+
|
|
46
|
+
// Step 1: Check with Mandatum
|
|
47
|
+
const approval = await this.mandatum.agents.spend(this.agentId, {
|
|
48
|
+
amount: amountInRupees,
|
|
49
|
+
currency,
|
|
50
|
+
vendor: 'razorpay',
|
|
51
|
+
category: 'payment',
|
|
52
|
+
description: description || `Razorpay payment of ${currency} ${amountInRupees}`,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!approval.allowed) {
|
|
56
|
+
return {
|
|
57
|
+
allowed: false,
|
|
58
|
+
reason: approval.reason,
|
|
59
|
+
eventId: approval.eventId,
|
|
60
|
+
spending: approval.spending,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Step 2: Mandatum approved — proceed with Razorpay
|
|
65
|
+
try {
|
|
66
|
+
const payment = await this.razorpay.orders.create({
|
|
67
|
+
amount,
|
|
68
|
+
currency,
|
|
69
|
+
notes: { ...notes, mandatum_event_id: approval.eventId },
|
|
70
|
+
...rest,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
allowed: true,
|
|
75
|
+
payment,
|
|
76
|
+
eventId: approval.eventId,
|
|
77
|
+
spending: approval.spending,
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Payment failed after Mandatum approved — should ideally refund the spending event
|
|
81
|
+
return {
|
|
82
|
+
allowed: true,
|
|
83
|
+
paymentFailed: true,
|
|
84
|
+
error: err.message,
|
|
85
|
+
eventId: approval.eventId,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a payment link — checks Mandatum limits first.
|
|
92
|
+
*/
|
|
93
|
+
async createPaymentLink({ amount, currency = 'INR', description, customer, ...rest }) {
|
|
94
|
+
const amountInRupees = amount / 100;
|
|
95
|
+
|
|
96
|
+
const approval = await this.mandatum.agents.spend(this.agentId, {
|
|
97
|
+
amount: amountInRupees,
|
|
98
|
+
currency,
|
|
99
|
+
vendor: 'razorpay',
|
|
100
|
+
category: 'payment-link',
|
|
101
|
+
description: description || `Payment link for ${currency} ${amountInRupees}`,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!approval.allowed) {
|
|
105
|
+
return { allowed: false, reason: approval.reason, eventId: approval.eventId };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const link = await this.razorpay.paymentLink.create({
|
|
109
|
+
amount, currency, description, customer,
|
|
110
|
+
notes: { mandatum_event_id: approval.eventId },
|
|
111
|
+
...rest,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return { allowed: true, paymentLink: link, eventId: approval.eventId };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default MandatumRazorpay;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandatum Stripe Payment Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps Stripe API calls with Mandatum authorization.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import Stripe from 'stripe';
|
|
8
|
+
* import { Mandatum } from '@mandatum/sdk';
|
|
9
|
+
* import { MandatumStripe } from '@mandatum/sdk/payments/stripe';
|
|
10
|
+
*
|
|
11
|
+
* const stripe = new Stripe('sk_live_...');
|
|
12
|
+
* const mandatum = new Mandatum({ apiKey: 'mdt_live_...' });
|
|
13
|
+
*
|
|
14
|
+
* const pay = new MandatumStripe(stripe, mandatum, 'agt_abc123');
|
|
15
|
+
* const result = await pay.createPaymentIntent({ amount: 5000, currency: 'usd' });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export class MandatumStripe {
|
|
19
|
+
constructor(stripeClient, mandatumClient, agentId) {
|
|
20
|
+
this.stripe = stripeClient;
|
|
21
|
+
this.mandatum = mandatumClient;
|
|
22
|
+
this.agentId = agentId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a PaymentIntent — checks Mandatum limits first.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} params
|
|
29
|
+
* @param {number} params.amount - Amount in smallest currency unit (cents for USD)
|
|
30
|
+
* @param {string} [params.currency='usd'] - Currency
|
|
31
|
+
* @param {string} [params.description] - Payment description
|
|
32
|
+
* @param {object} [params.metadata] - Stripe metadata
|
|
33
|
+
*/
|
|
34
|
+
async createPaymentIntent({ amount, currency = 'usd', description, metadata, ...rest }) {
|
|
35
|
+
// Convert cents to dollars for Mandatum
|
|
36
|
+
const amountInMajor = amount / 100;
|
|
37
|
+
|
|
38
|
+
const approval = await this.mandatum.agents.spend(this.agentId, {
|
|
39
|
+
amount: amountInMajor,
|
|
40
|
+
currency: currency.toUpperCase(),
|
|
41
|
+
vendor: 'stripe',
|
|
42
|
+
category: 'payment',
|
|
43
|
+
description: description || `Stripe payment of ${currency.toUpperCase()} ${amountInMajor}`,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!approval.allowed) {
|
|
47
|
+
return {
|
|
48
|
+
allowed: false,
|
|
49
|
+
reason: approval.reason,
|
|
50
|
+
eventId: approval.eventId,
|
|
51
|
+
spending: approval.spending,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const paymentIntent = await this.stripe.paymentIntents.create({
|
|
57
|
+
amount,
|
|
58
|
+
currency,
|
|
59
|
+
description,
|
|
60
|
+
metadata: { ...metadata, mandatum_event_id: approval.eventId },
|
|
61
|
+
...rest,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
allowed: true,
|
|
66
|
+
paymentIntent,
|
|
67
|
+
eventId: approval.eventId,
|
|
68
|
+
spending: approval.spending,
|
|
69
|
+
};
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
allowed: true,
|
|
73
|
+
paymentFailed: true,
|
|
74
|
+
error: err.message,
|
|
75
|
+
eventId: approval.eventId,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a Checkout Session — checks Mandatum limits first.
|
|
82
|
+
*/
|
|
83
|
+
async createCheckoutSession({ lineItems, totalAmount, currency = 'usd', ...rest }) {
|
|
84
|
+
const amountInMajor = totalAmount / 100;
|
|
85
|
+
|
|
86
|
+
const approval = await this.mandatum.agents.spend(this.agentId, {
|
|
87
|
+
amount: amountInMajor,
|
|
88
|
+
currency: currency.toUpperCase(),
|
|
89
|
+
vendor: 'stripe',
|
|
90
|
+
category: 'checkout',
|
|
91
|
+
description: `Stripe checkout for ${currency.toUpperCase()} ${amountInMajor}`,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!approval.allowed) {
|
|
95
|
+
return { allowed: false, reason: approval.reason, eventId: approval.eventId };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const session = await this.stripe.checkout.sessions.create({
|
|
99
|
+
line_items: lineItems,
|
|
100
|
+
mode: 'payment',
|
|
101
|
+
metadata: { mandatum_event_id: approval.eventId },
|
|
102
|
+
...rest,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { allowed: true, session, eventId: approval.eventId };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default MandatumStripe;
|