@grantex/sdk 0.5.1 → 0.6.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 CHANGED
@@ -1,942 +1,947 @@
1
- # @grantex/sdk
2
-
3
- TypeScript SDK for the [Grantex](https://grantex.dev) delegated authorization protocol — OAuth 2.0 for AI agents.
4
-
5
- [![npm version](https://img.shields.io/npm/v/@grantex/sdk)](https://www.npmjs.com/package/@grantex/sdk)
6
- [![License](https://img.shields.io/npm/l/@grantex/sdk)](https://github.com/mishrasanjeev/grantex/blob/main/LICENSE)
7
-
8
- > **[Homepage](https://grantex.dev)** | **[Docs](https://docs.grantex.dev)** | **[API Reference](https://docs.grantex.dev/api-reference)** | **[Sign Up Free](https://grantex.dev/dashboard/signup)** | **[GitHub](https://github.com/mishrasanjeev/grantex)**
9
-
10
- ## Installation
11
-
12
- ```bash
13
- npm install @grantex/sdk
14
- ```
15
-
16
- ## Quick Start
17
-
18
- ```typescript
19
- import { Grantex, verifyGrantToken } from '@grantex/sdk';
20
-
21
- const grantex = new Grantex({ apiKey: 'YOUR_API_KEY' });
22
-
23
- // 1. Register an agent
24
- const agent = await grantex.agents.register({
25
- name: 'Email Assistant',
26
- description: 'Reads and sends email on behalf of users',
27
- scopes: ['email:read', 'email:send'],
28
- });
29
-
30
- // 2. Request authorization
31
- const { consentUrl } = await grantex.authorize({
32
- agentId: agent.id,
33
- userId: 'usr_01J...',
34
- scopes: ['email:read', 'email:send'],
35
- });
36
- // Redirect the user to consentUrl — they approve in plain language
37
-
38
- // 3. Exchange authorization code for a grant token
39
- // (your redirect callback receives the `code` after user approves)
40
- const token = await grantex.tokens.exchange({ code, agentId: agent.id });
41
- console.log(token.grantToken); // RS256-signed JWT
42
- console.log(token.scopes); // ['email:read', 'email:send']
43
- console.log(token.grantId); // 'grnt_01J...'
44
-
45
- // 4. Verify locally using keys retrieved from the issuer's JWKS
46
- const grant = await verifyGrantToken(token.grantToken, {
47
- jwksUri: 'https://api.grantex.dev/.well-known/jwks.json',
48
- });
49
- console.log(grant.principalId); // 'usr_01J...'
50
-
51
- // 5. Revoke when done
52
- await grantex.tokens.revoke(grant.tokenId);
53
- ```
54
-
55
- ## Configuration
56
-
57
- ```typescript
58
- const grantex = new Grantex({
59
- apiKey: 'gx_....', // or set GRANTEX_API_KEY env var
60
- baseUrl: 'https://api.grantex.dev', // default
61
- issuer: 'https://grantex.dev', // optional when token issuer differs from API host
62
- jwksUri: 'https://api.grantex.dev/.well-known/jwks.json', // optional JWKS override
63
- timeout: 30000, // request timeout in ms (default: 30s)
64
- });
65
- ```
66
-
67
- | Option | Type | Default | Description |
68
- |--------|------|---------|-------------|
69
- | `apiKey` | `string` | `process.env.GRANTEX_API_KEY` | API key for authentication |
70
- | `baseUrl` | `string` | `https://api.grantex.dev` | Base URL of the Grantex API |
71
- | `issuer` | `string` | derived from `jwksUri` | Expected JWT issuer for local signature verification |
72
- | `jwksUri` | `string` | `${baseUrl}/.well-known/jwks.json` | URL from which the verifier retrieves signing keys |
73
- | `timeout` | `number` | `30000` | Request timeout in milliseconds |
74
-
75
- ## OAuth Agent Profile Client
76
-
77
- `OAuthAgentClient` implements the client role in the prepared
78
- `draft-mishra-oauth-agent-grants-03` profile. It discovers and validates RFC
79
- 8414 metadata, creates an ES256 DPoP key by default, uses PAR and PKCE S256,
80
- validates `state` and the RFC 9207 `iss` response parameter, rotates refresh
81
- tokens, performs same-resource RFC 8693 attenuation, and creates DPoP proofs
82
- for protected-resource requests.
83
-
84
- ```typescript
85
- import { OAuthAgentClient } from '@grantex/sdk';
86
-
87
- const client = await OAuthAgentClient.create({
88
- issuer: 'https://grantex.dev',
89
- clientId: 'ag_01J...',
90
- redirectUri: 'https://agent.example/callback',
91
- resource: 'https://api.example/resource',
92
- });
93
-
94
- const pending = await client.beginAuthorization({
95
- scopes: ['grantex.resource.read'],
96
- principalHint: 'principal@example.com',
97
- });
98
-
99
- // Redirect the Principal to pending.authorizationUrl. In the callback:
100
- const tokens = await client.completeAuthorization(callbackUrl);
101
-
102
- const response = await client.fetch(
103
- 'https://api.example/resource',
104
- tokens.access_token,
105
- );
106
-
107
- const narrower = await client.attenuate(tokens.access_token, [
108
- 'grantex.resource.read',
109
- ]);
110
- const refreshAttempt = crypto.randomUUID();
111
- const rotated = await client.refresh(tokens.refresh_token!, {
112
- idempotencyKey: refreshAttempt,
113
- });
114
- await client.revoke(rotated.refresh_token!, 'refresh_token');
115
- ```
116
-
117
- Persist the idempotency key with the old refresh token when lost-response
118
- recovery must survive a caller restart. Repeating both within 300 seconds uses
119
- a fresh DPoP proof and returns the exact committed token values with a
120
- recalculated, non-extended `expires_in`. A
121
- different key or DPoP identity is treated as refresh-token reuse and revokes
122
- the token family. When no key is supplied, the client generates and retains one
123
- for the old refresh token for five minutes in the current process.
124
-
125
- Persist the generated key securely if an instance must survive process
126
- restarts. Supply the matching `privateKey` and `publicJwk` to `create`; both are
127
- required together. `principalHint` is optional account-discovery input and is
128
- not proof of the Principal's identity; live approval still requires the
129
- authorization server's passkey authentication. Plain HTTP endpoints are rejected unless
130
- `allowInsecureLoopback` is enabled for local loopback testing. Revision `-02`
131
- of the draft family is published as an active individual Internet-Draft;
132
- revision `-03` is the working candidate implemented here. Neither is an
133
- IETF-endorsed or independently certified standard.
134
-
135
- ## Agent prepaid wallets (SDK 0.5+)
136
-
137
- `PrepaidWalletAgentClient` uses an `OAuthAgentClient` and DPoP access token to
138
- list assigned wallets, reserve payments, and request threshold reloads.
139
- `PrincipalPrepaidWalletClient` uses a short-lived principal-session token to
140
- create and fund wallets, assign safe-default policy, manage layered spend
141
- policies and exact payment approvals, approve reloads, inspect activity, and
142
- block an assignment, wallet, or all wallets for one agent.
143
- `DeveloperPrepaidWalletPolicyClient` manages tenant-level policy with the
144
- developer API key.
145
-
146
- The access token must include `wallet:spend` and each action scope used in a
147
- payment (for example `weather:read`). Agent wallet listings intentionally omit
148
- custody-provider IDs, wallet addresses, principal IDs, and wallet metadata.
149
-
150
- ```typescript
151
- import {
152
- PrepaidWalletAgentClient,
153
- PrincipalPrepaidWalletClient,
154
- } from '@grantex/sdk';
155
-
156
- const agentWallets = new PrepaidWalletAgentClient({
157
- oauthClient,
158
- accessToken,
159
- });
160
-
161
- const principalWallets = new PrincipalPrepaidWalletClient({
162
- baseUrl: 'https://grantex.dev',
163
- sessionToken,
164
- });
165
-
166
- await principalWallets.createSpendPolicy({
167
- name: 'Shared research budget',
168
- scopeType: 'group',
169
- scopeId: 'research-agents',
170
- effect: 'limit',
171
- maxAmount: '1000000',
172
- windowType: 'month',
173
- onExceed: 'require_approval',
174
- purposes: ['research'],
175
- });
176
-
177
- const authorization = await agentWallets.authorizePayment({
178
- amount: '1000',
179
- asset: 'USDC',
180
- network: 'grantex:prepaid',
181
- recipient: 'merchant:weather-api',
182
- resource: 'https://merchant.example/weather',
183
- scope: 'weather:read',
184
- merchantId: 'merchant:weather-api',
185
- purpose: 'research',
186
- projectId: 'climate-2026',
187
- costCenter: 'engineering',
188
- maxTimeoutSeconds: 120,
189
- idempotencyKey: crypto.randomUUID(),
190
- });
191
- ```
192
-
193
- `authorizePayment` returns either a signed reservation or an
194
- `approval_required` response. After the principal approves that exact request,
195
- retry with its `approvalRequestId`, the same wallet, idempotency key, and all
196
- original payment fields. Approval is short-lived and single-use.
197
-
198
- Amounts are atomic-unit integer strings. Layered policy and exact approval are
199
- available in `@grantex/sdk` 0.5.0 and later.
200
-
201
- Self-hosted wallet deployments must also provide correct public resource
202
- routing, migrations `091` and `092`, durable notification delivery, merchant-side
203
- idempotency, and any external custody/provider integration. See [Prepaid Wallet
204
- Production
205
- Readiness](https://docs.grantex.dev/guides/prepaid-wallet-production);
206
- installing the SDK alone does not provide those dependencies.
207
-
208
- ## Commerce V1 / OACP
209
-
210
- The SDK includes a `commerce` resource for the Grantex Commerce V1 control
211
- plane and OACP live-pilot flow.
212
-
213
- ```typescript
214
- import { Grantex } from '@grantex/sdk';
215
-
216
- const grantex = new Grantex({ apiKey: process.env.GRANTEX_API_KEY! });
217
-
218
- // Public merchant publishing profile
219
- const profile = await grantex.commerce.getProfile({
220
- merchantId: 'mch_shopify_mgx0n6_22',
221
- });
222
- console.log(profile.merchant?.merchant_id);
223
-
224
- // Catalog grounding
225
- const catalog = await grantex.commerce.searchCatalog({
226
- merchant_id: 'mch_shopify_mgx0n6_22',
227
- query: 'shirt',
228
- limit: 5,
229
- });
230
-
231
- // Agent cart creation. Commerce write paths require Idempotency-Key.
232
- const cart = await grantex.commerce.createCart({
233
- idempotencyKey: crypto.randomUUID(),
234
- merchant_id: 'mch_shopify_mgx0n6_22',
235
- currency: 'INR',
236
- line_items: [
237
- { variant_id: String(catalog.items[0]?.['variant_id']), quantity: 1 },
238
- ],
239
- });
240
-
241
- // Consent request and Commerce Passport exchange
242
- const consent = await grantex.commerce.createConsentRequest({
243
- merchant_id: 'mch_shopify_mgx0n6_22',
244
- passport_type: 'checkout',
245
- max_amount: Number(cart.data['total_amount']),
246
- currency: 'INR',
247
- });
248
-
249
- // Redirect the buyer to consent.data['consent_url'], then exchange after
250
- // the consent request is granted.
251
- const passport = await grantex.commerce.exchangeConsentForPassport({
252
- consent_request_id: String(consent.data['consent_request_id']),
253
- });
254
-
255
- const payment = await grantex.commerce.createPaymentIntent({
256
- idempotencyKey: crypto.randomUUID(),
257
- merchant_id: 'mch_shopify_mgx0n6_22',
258
- cart_id: String(cart.data['cart_id']),
259
- passport_jwt: String(passport.data['passport_jwt']),
260
- amount_minor_units: Number(cart.data['total_amount']),
261
- currency: 'INR',
262
- provider_key: 'plural',
263
- });
264
-
265
- const checkout = await grantex.commerce.createCheckoutLink(
266
- String(payment.data['payment_intent_id']),
267
- {
268
- idempotencyKey: crypto.randomUUID(),
269
- passport_jwt: String(passport.data['passport_jwt']),
270
- success_url: 'https://buyer.example/success',
271
- cancel_url: 'https://buyer.example/cancel',
272
- },
273
- );
274
- ```
275
-
276
- Plural webhook intake is available at
277
- `https://api.grantex.dev/v1/webhooks/providers/plural`. Provider webhooks are
278
- normally called by the provider dashboard, not by application code. The SDK
279
- also exposes `grantex.commerce.getOpsHealth()` and
280
- `grantex.commerce.listProviderWebhookEvents()` for operator health checks.
281
-
282
- ## PKCE Support
283
-
284
- The SDK includes built-in PKCE (Proof Key for Code Exchange) support using the S256 method for secure authorization flows:
285
-
286
- ```typescript
287
- import { Grantex, generatePkce } from '@grantex/sdk';
288
-
289
- const grantex = new Grantex({ apiKey: 'YOUR_API_KEY' });
290
-
291
- // 1. Generate a PKCE challenge
292
- const pkce = generatePkce();
293
- // pkce.codeVerifier — random 43-char string (keep secret)
294
- // pkce.codeChallenge — SHA-256 hash of verifier (send to server)
295
- // pkce.codeChallengeMethod — 'S256'
296
-
297
- // 2. Pass the challenge when requesting authorization
298
- const { consentUrl } = await grantex.authorize({
299
- agentId: 'ag_01J...',
300
- userId: 'usr_01J...',
301
- scopes: ['files:read'],
302
- codeChallenge: pkce.codeChallenge,
303
- codeChallengeMethod: pkce.codeChallengeMethod,
304
- });
305
-
306
- // 3. Exchange the code with the verifier
307
- const token = await grantex.tokens.exchange({
308
- code: 'auth_code_from_redirect',
309
- agentId: 'ag_01J...',
310
- codeVerifier: pkce.codeVerifier,
311
- });
312
- ```
313
-
314
- ---
315
-
316
- ## API Reference
317
-
318
- ### Authorization
319
-
320
- #### `grantex.authorize(params)`
321
-
322
- Initiate the delegated authorization flow. Returns a consent URL to redirect the user to.
323
-
324
- ```typescript
325
- const request = await grantex.authorize({
326
- agentId: 'ag_01J...',
327
- userId: 'usr_01J...',
328
- scopes: ['files:read', 'email:send'],
329
- audience: 'https://api.example.com', // optional; becomes the JWT aud claim
330
- expiresIn: '24h', // optional
331
- redirectUri: 'https://...' // optional
332
- });
333
-
334
- console.log(request.consentUrl); // redirect user here
335
- console.log(request.authRequestId); // track the request
336
- console.log(request.expiresAt); // ISO 8601 timestamp
337
- ```
338
-
339
- **Returns**: `AuthorizationRequest`
340
-
341
- | Field | Type | Description |
342
- |-------|------|-------------|
343
- | `authRequestId` | `string` | Unique ID for this authorization request |
344
- | `consentUrl` | `string` | URL to redirect the user to for consent |
345
- | `agentId` | `string` | The agent requesting authorization |
346
- | `principalId` | `string` | The user being asked for consent |
347
- | `scopes` | `string[]` | Requested scopes |
348
- | `expiresAt` | `string` | When the request expires (ISO 8601) |
349
- | `status` | `string` | `'pending'`, `'approved'`, `'denied'`, or `'expired'` |
350
-
351
- ---
352
-
353
- ### Agents
354
-
355
- #### `grantex.agents.register(params)`
356
-
357
- Register a new AI agent.
358
-
359
- ```typescript
360
- const agent = await grantex.agents.register({
361
- name: 'Code Review Bot',
362
- description: 'Reviews pull requests and suggests improvements',
363
- scopes: ['repo:read', 'pr:comment'],
364
- });
365
- ```
366
-
367
- #### `grantex.agents.get(agentId)`
368
-
369
- ```typescript
370
- const agent = await grantex.agents.get('ag_01J...');
371
- ```
372
-
373
- #### `grantex.agents.list()`
374
-
375
- ```typescript
376
- const { agents } = await grantex.agents.list();
377
- ```
378
-
379
- #### `grantex.agents.update(agentId, params)`
380
-
381
- ```typescript
382
- const agent = await grantex.agents.update('ag_01J...', {
383
- name: 'Updated Name',
384
- scopes: ['repo:read', 'pr:comment', 'pr:approve'],
385
- });
386
- ```
387
-
388
- #### `grantex.agents.delete(agentId)`
389
-
390
- ```typescript
391
- await grantex.agents.delete('ag_01J...');
392
- ```
393
-
394
- ---
395
-
396
- ### Grants
397
-
398
- #### `grantex.grants.get(grantId)`
399
-
400
- ```typescript
401
- const grant = await grantex.grants.get('grnt_01J...');
402
- ```
403
-
404
- #### `grantex.grants.list(params?)`
405
-
406
- ```typescript
407
- const { grants } = await grantex.grants.list({
408
- agentId: 'ag_01J...', // optional filter
409
- principalId: 'usr_01J...', // optional filter
410
- status: 'active', // 'active' | 'revoked' | 'expired'
411
- page: 1,
412
- pageSize: 20,
413
- });
414
- ```
415
-
416
- #### `grantex.grants.revoke(grantId)`
417
-
418
- ```typescript
419
- await grantex.grants.revoke('grnt_01J...');
420
- ```
421
-
422
- #### `grantex.grants.delegate(params)`
423
-
424
- Create a delegated sub-agent grant (per [SPEC Section 9](https://github.com/mishrasanjeev/grantex/blob/main/SPEC.md)).
425
-
426
- ```typescript
427
- const delegation = await grantex.grants.delegate({
428
- parentGrantToken: 'eyJhbG...',
429
- subAgentId: 'ag_02K...',
430
- scopes: ['files:read'], // must be subset of parent scopes
431
- expiresIn: '1h', // optional, cannot exceed parent
432
- });
433
-
434
- console.log(delegation.grantToken); // new JWT for the sub-agent
435
- console.log(delegation.grantId);
436
- ```
437
-
438
- #### `grantex.grants.verify(token)`
439
-
440
- Verify a grant token via the API (online verification with real-time revocation check).
441
-
442
- ```typescript
443
- const verified = await grantex.grants.verify('eyJhbG...');
444
- console.log(verified.principalId);
445
- console.log(verified.scopes);
446
- ```
447
-
448
- Throws `GrantexTokenError` when the token is inactive, revoked, expired, or otherwise unusable.
449
-
450
- ---
451
-
452
- ### Tokens
453
-
454
- #### `grantex.tokens.exchange(params)`
455
-
456
- Exchange an authorization code for a grant token. This is the standard way to obtain a grant token after the user approves the consent request.
457
-
458
- ```typescript
459
- const token = await grantex.tokens.exchange({
460
- code: 'auth_code_from_redirect', // from your redirect callback
461
- agentId: 'ag_01J...',
462
- });
463
-
464
- console.log(token.grantToken); // RS256-signed JWT — pass this to your agent
465
- console.log(token.grantId); // grant record ID
466
- console.log(token.scopes); // granted scopes
467
- console.log(token.expiresAt); // ISO 8601 expiry
468
- console.log(token.refreshToken); // for token refresh
469
- ```
470
-
471
- **Returns**: `ExchangeTokenResponse`
472
-
473
- | Field | Type | Description |
474
- |-------|------|-------------|
475
- | `grantToken` | `string` | Signed RS256 JWT — the agent's bearer credential |
476
- | `grantId` | `string` | Grant record ID |
477
- | `scopes` | `string[]` | Scopes the user approved |
478
- | `expiresAt` | `string` | Underlying grant expiry (ISO 8601) |
479
- | `refreshToken` | `string` | Refresh token for rotating credentials while the grant remains active |
480
-
481
- Refresh tokens are single-use and rotate on every accepted refresh. If a response is lost after commit, retry the same previous refresh token and idempotency key. The SDK retains an omitted key for five minutes in the current process; persist an explicit `idempotencyKey` with the old token when recovery must survive restart or failover. Grantex returns the already-rotated pair without extending `expiresAt`; after grant expiry, re-authorize.
482
-
483
- ---
484
-
485
- #### `grantex.tokens.verify(token)`
486
-
487
- Online token verification with revocation status.
488
-
489
- ```typescript
490
- const result = await grantex.tokens.verify('eyJhbG...');
491
- if (result.valid) {
492
- console.log(result.scopes); // ['files:read']
493
- console.log(result.principal); // 'usr_01J...'
494
- console.log(result.agent); // 'ag_01J...'
495
- console.log(result.grantId);
496
- console.log(result.expiresAt);
497
- }
498
- ```
499
-
500
- #### `grantex.tokens.revoke(tokenId)`
501
-
502
- Revoke a token by its JTI. Blocklisted in Redis immediately; all sub-delegated tokens are also invalidated.
503
-
504
- ```typescript
505
- await grantex.tokens.revoke('tok_01J...');
506
- ```
507
-
508
- ---
509
-
510
- ### Local Token Verification
511
-
512
- #### `verifyGrantToken(token, options)`
513
-
514
- Verify a grant token locally with RS256 signing keys retrieved from the published JWKS URL. A bounded process-level resolver cache reuses valid keys for each normalized JWKS URI; initial retrieval and key-rotation refreshes may require network access.
515
-
516
- ```typescript
517
- import { verifyGrantToken } from '@grantex/sdk';
518
-
519
- const grant = await verifyGrantToken('eyJhbG...', {
520
- jwksUri: 'https://api.grantex.dev/.well-known/jwks.json',
521
- issuer: 'https://grantex.dev', // optional when issuer differs from JWKS host
522
- requiredScopes: ['files:read'], // optional — rejects if missing
523
- audience: 'https://myapp.com', // optional — validates aud claim
524
- });
525
- ```
526
-
527
- If you call a deployment through a raw Cloud Run URL or another internal host, but the service signs tokens for a canonical public domain, pass `issuer` explicitly. Otherwise issuer validation will reject a valid token because the JWT `iss` claim will not match the transport host.
528
-
529
- **Returns**: `VerifiedGrant`
530
-
531
- | Field | Type | Description |
532
- |-------|------|-------------|
533
- | `tokenId` | `string` | Unique token ID (JWT `jti` claim) |
534
- | `grantId` | `string` | Grant record ID |
535
- | `principalId` | `string` | User who authorized the grant (`sub` claim) |
536
- | `agentDid` | `string` | Agent's DID (`agt` claim) |
537
- | `developerId` | `string` | Developer org ID (`dev` claim) |
538
- | `scopes` | `string[]` | Granted scopes (`scp` claim) |
539
- | `issuedAt` | `number` | Issued-at timestamp (seconds since epoch) |
540
- | `expiresAt` | `number` | Expiry timestamp (seconds since epoch) |
541
- | `parentAgentDid` | `string?` | Parent agent DID (delegation only) |
542
- | `parentGrantId` | `string?` | Parent grant ID (delegation only) |
543
- | `delegationDepth` | `number?` | Delegation depth (0 = root) |
544
-
545
- ---
546
-
547
- ### Audit
548
-
549
- #### `grantex.audit.log(params)`
550
-
551
- Log an auditable action taken by an agent.
552
-
553
- ```typescript
554
- const entry = await grantex.audit.log({
555
- agentId: 'ag_01J...',
556
- agentDid: 'did:grantex:ag_01J...',
557
- grantId: 'grnt_01J...',
558
- principalId: 'usr_01J...',
559
- action: 'email:send',
560
- metadata: { to: 'user@example.com', subject: 'Hello' },
561
- status: 'success', // 'success' | 'failure' | 'blocked'
562
- });
563
- ```
564
-
565
- #### `grantex.audit.list(params?)`
566
-
567
- ```typescript
568
- const { entries } = await grantex.audit.list({
569
- agentId: 'ag_01J...',
570
- action: 'email:send',
571
- since: '2026-01-01T00:00:00Z',
572
- until: '2026-02-28T23:59:59Z',
573
- page: 1,
574
- pageSize: 50,
575
- });
576
- ```
577
-
578
- #### `grantex.audit.get(entryId)`
579
-
580
- ```typescript
581
- const entry = await grantex.audit.get('alog_01J...');
582
- console.log(entry.hash); // SHA-256 hash for tamper evidence
583
- console.log(entry.prevHash); // previous entry hash (chain integrity)
584
- ```
585
-
586
- ---
587
-
588
- ### Webhooks
589
-
590
- #### `grantex.webhooks.create(params)`
591
-
592
- ```typescript
593
- const webhook = await grantex.webhooks.create({
594
- url: 'https://myapp.com/webhooks/grantex',
595
- events: ['grant.created', 'grant.revoked', 'token.issued'],
596
- });
597
- console.log(webhook.secret); // HMAC secret for signature verification
598
- ```
599
-
600
- #### `grantex.webhooks.list()`
601
-
602
- ```typescript
603
- const { webhooks } = await grantex.webhooks.list();
604
- ```
605
-
606
- #### `grantex.webhooks.delete(webhookId)`
607
-
608
- ```typescript
609
- await grantex.webhooks.delete('wh_01J...');
610
- ```
611
-
612
- #### Webhook Signature Verification
613
-
614
- ```typescript
615
- import { verifyWebhookSignature } from '@grantex/sdk';
616
-
617
- // In your webhook handler
618
- verifyWebhookSignature(requestBody, signatureHeader, webhookSecret);
619
- ```
620
-
621
- ---
622
-
623
- ### Policies
624
-
625
- Define fine-grained access control rules for agents.
626
-
627
- #### `grantex.policies.create(params)`
628
-
629
- ```typescript
630
- const policy = await grantex.policies.create({
631
- name: 'Block after hours',
632
- effect: 'deny',
633
- priority: 10,
634
- scopes: ['email:send'],
635
- timeOfDayStart: '18:00',
636
- timeOfDayEnd: '08:00',
637
- });
638
- ```
639
-
640
- #### `grantex.policies.list()`
641
-
642
- ```typescript
643
- const { policies, total } = await grantex.policies.list();
644
- ```
645
-
646
- #### `grantex.policies.get(policyId)` / `update(policyId, params)` / `delete(policyId)`
647
-
648
- ```typescript
649
- const policy = await grantex.policies.get('pol_01J...');
650
-
651
- await grantex.policies.update('pol_01J...', { effect: 'allow' });
652
-
653
- await grantex.policies.delete('pol_01J...');
654
- ```
655
-
656
- ---
657
-
658
- ### Compliance
659
-
660
- #### `grantex.compliance.getSummary(params?)`
661
-
662
- ```typescript
663
- const summary = await grantex.compliance.getSummary({
664
- since: '2026-01-01T00:00:00Z',
665
- until: '2026-02-28T23:59:59Z',
666
- });
667
- console.log(summary.agents); // { total, active, suspended, revoked }
668
- console.log(summary.grants); // { total, active, revoked, expired }
669
- console.log(summary.auditEntries); // { total, success, failure, blocked }
670
- ```
671
-
672
- #### `grantex.compliance.exportGrants(params?)`
673
-
674
- ```typescript
675
- const { grants, total } = await grantex.compliance.exportGrants({
676
- status: 'active',
677
- });
678
- ```
679
-
680
- #### `grantex.compliance.exportAudit(params?)`
681
-
682
- ```typescript
683
- const { entries, total } = await grantex.compliance.exportAudit({
684
- since: '2026-01-01T00:00:00Z',
685
- agentId: 'ag_01J...',
686
- });
687
- ```
688
-
689
- #### `grantex.compliance.evidencePack(params?)`
690
-
691
- Generate a full SOC 2 / GDPR evidence pack with audit chain integrity verification.
692
-
693
- ```typescript
694
- const pack = await grantex.compliance.evidencePack({
695
- framework: 'soc2', // 'soc2' | 'gdpr' | 'all'
696
- since: '2026-01-01T00:00:00Z',
697
- });
698
-
699
- console.log(pack.chainIntegrity.valid); // true
700
- console.log(pack.chainIntegrity.checkedEntries); // 1042
701
- console.log(pack.summary);
702
- console.log(pack.grants);
703
- console.log(pack.auditEntries);
704
- console.log(pack.policies);
705
- ```
706
-
707
- ---
708
-
709
- ### Anomaly Detection
710
-
711
- #### `grantex.anomalies.detect()`
712
-
713
- Run anomaly detection across all agents.
714
-
715
- ```typescript
716
- const { anomalies, total } = await grantex.anomalies.detect();
717
- // anomaly types: 'rate_spike' | 'high_failure_rate' | 'new_principal' | 'off_hours_activity'
718
- ```
719
-
720
- #### `grantex.anomalies.list(params?)`
721
-
722
- ```typescript
723
- const { anomalies } = await grantex.anomalies.list({
724
- unacknowledged: true, // only open anomalies
725
- });
726
- ```
727
-
728
- #### `grantex.anomalies.acknowledge(anomalyId)`
729
-
730
- ```typescript
731
- const anomaly = await grantex.anomalies.acknowledge('anom_01J...');
732
- ```
733
-
734
- ---
735
-
736
- ### Billing
737
-
738
- #### `grantex.billing.getSubscription()`
739
-
740
- ```typescript
741
- const sub = await grantex.billing.getSubscription();
742
- console.log(sub.plan); // 'free' | 'pro' | 'enterprise'
743
- console.log(sub.status); // 'active' | 'past_due' | 'canceled'
744
- console.log(sub.currentPeriodEnd); // ISO 8601 or null
745
- ```
746
-
747
- #### `grantex.billing.createCheckout(params)`
748
-
749
- ```typescript
750
- const { checkoutUrl } = await grantex.billing.createCheckout({
751
- plan: 'pro',
752
- successUrl: 'https://myapp.com/billing/success',
753
- cancelUrl: 'https://myapp.com/billing/cancel',
754
- });
755
- // Redirect user to checkoutUrl
756
- ```
757
-
758
- #### `grantex.billing.createPortal(params)`
759
-
760
- ```typescript
761
- const { portalUrl } = await grantex.billing.createPortal({
762
- returnUrl: 'https://myapp.com/settings',
763
- });
764
- ```
765
-
766
- ---
767
-
768
- ### SCIM 2.0 Provisioning
769
-
770
- Sync users from your identity provider.
771
-
772
- #### Token Management
773
-
774
- ```typescript
775
- // Create a SCIM bearer token
776
- const { token, id, label } = await grantex.scim.createToken({
777
- label: 'Okta SCIM integration',
778
- });
779
- // token is returned once — store it securely
780
-
781
- const { tokens } = await grantex.scim.listTokens();
782
-
783
- await grantex.scim.revokeToken('scimtok_01J...');
784
- ```
785
-
786
- #### User Operations
787
-
788
- ```typescript
789
- // List provisioned users
790
- const { Resources, totalResults } = await grantex.scim.listUsers({
791
- startIndex: 1,
792
- count: 100,
793
- });
794
-
795
- // Create a user
796
- const user = await grantex.scim.createUser({
797
- userName: 'alice@example.com',
798
- displayName: 'Alice',
799
- emails: [{ value: 'alice@example.com', primary: true }],
800
- });
801
-
802
- // Get / Replace / Patch / Delete
803
- const user = await grantex.scim.getUser('scimusr_01J...');
804
-
805
- await grantex.scim.replaceUser('scimusr_01J...', { userName: 'alice@new.com' });
806
-
807
- await grantex.scim.updateUser('scimusr_01J...', [
808
- { op: 'replace', path: 'active', value: false },
809
- ]);
810
-
811
- await grantex.scim.deleteUser('scimusr_01J...');
812
- ```
813
-
814
- ---
815
-
816
- ### SSO (OIDC)
817
-
818
- #### `grantex.sso.createConfig(params)`
819
-
820
- ```typescript
821
- const config = await grantex.sso.createConfig({
822
- issuerUrl: 'https://accounts.google.com',
823
- clientId: 'xxx.apps.googleusercontent.com',
824
- clientSecret: 'GOCSPX-...',
825
- redirectUri: 'https://myapp.com/auth/callback',
826
- });
827
- ```
828
-
829
- #### `grantex.sso.getConfig()` / `deleteConfig()`
830
-
831
- ```typescript
832
- const config = await grantex.sso.getConfig();
833
- await grantex.sso.deleteConfig();
834
- ```
835
-
836
- #### `grantex.sso.getLoginUrl(org)`
837
-
838
- ```typescript
839
- const { authorizeUrl } = await grantex.sso.getLoginUrl('dev_01J...');
840
- // Redirect user to authorizeUrl
841
- ```
842
-
843
- #### `grantex.sso.handleCallback(code, state)`
844
-
845
- ```typescript
846
- const { email, name, sub, developerId } = await grantex.sso.handleCallback(code, state);
847
- ```
848
-
849
- ---
850
-
851
- ## Error Handling
852
-
853
- All errors extend `GrantexError`:
854
-
855
- ```typescript
856
- import {
857
- GrantexError, // base class
858
- GrantexApiError, // API returned an error (has statusCode, body, requestId)
859
- GrantexAuthError, // 401/403 — invalid or missing API key
860
- GrantexTokenError, // token verification failed (invalid signature, expired, etc.)
861
- GrantexNetworkError, // network failure (timeout, DNS, connection refused)
862
- } from '@grantex/sdk';
863
-
864
- try {
865
- await grantex.agents.get('ag_invalid');
866
- } catch (err) {
867
- if (err instanceof GrantexAuthError) {
868
- console.error('Auth failed:', err.statusCode); // 401 or 403
869
- console.error('Request ID:', err.requestId);
870
- } else if (err instanceof GrantexApiError) {
871
- console.error('API error:', err.statusCode, err.body);
872
- } else if (err instanceof GrantexNetworkError) {
873
- console.error('Network error:', err.message, err.cause);
874
- }
875
- }
876
- ```
877
-
878
- ---
879
-
880
- ## Requirements
881
-
882
- - Node.js 18+
883
- - ESM (`"type": "module"` in your package.json, or use dynamic `import()`)
884
-
885
- ## Links
886
-
887
- - [GitHub](https://github.com/mishrasanjeev/grantex)
888
- - [Protocol Specification](https://github.com/mishrasanjeev/grantex/blob/main/SPEC.md)
889
- - [Python SDK](https://pypi.org/project/grantex/)
890
- - [API Reference](https://api.grantex.dev/.well-known/jwks.json)
891
-
892
- ## Scope Enforcement (v0.3.1)
893
-
894
- Enforce tool-level permissions on **any connector** — define your own manifests or use the 53 pre-built ones.
895
-
896
- ```typescript
897
- import { Grantex, ToolManifest, Permission } from '@grantex/sdk';
898
-
899
- const grantex = new Grantex({ apiKey: 'gx_...' });
900
-
901
- // Define a manifest for any connector — no dependency on Grantex to add support
902
- grantex.loadManifest(new ToolManifest({
903
- connector: 'my-crm',
904
- tools: { search: Permission.READ, create_deal: Permission.WRITE, delete_account: Permission.DELETE },
905
- }));
906
-
907
- const result = await grantex.enforce({ grantToken: token, connector: 'my-crm', tool: 'delete_account' });
908
- // result.allowed = false — "write scope does not permit delete operations"
909
- ```
910
-
911
- **Features:**
912
- - `enforce()` — verify JWT + check tool permission via manifest, <1ms
913
- - `wrapTool()` — auto-enforce on LangChain tools
914
- - `enforceMiddleware()` — Express/Fastify HTTP middleware
915
- - Define custom manifests for any connector: inline, from JSON, or auto-generated via CLI
916
- - 53 pre-built manifests included (Salesforce, HubSpot, Jira, Stripe, SAP, S3, and 47 more)
917
- - Permission hierarchy: `admin > delete > write > read`
918
- - Permissive mode for migration (`enforceMode: 'permissive'`)
919
-
920
- [Full Guide](https://docs.grantex.dev/guides/scope-enforcement) | [API Reference](https://docs.grantex.dev/sdks/typescript/enforce)
921
-
922
- ## Grantex Ecosystem
923
-
924
- | Package | Description |
925
- |---|---|
926
- | [`grantex`](https://pypi.org/project/grantex/) | Python SDK |
927
- | [`@grantex/langchain`](https://www.npmjs.com/package/@grantex/langchain) | LangChain integration |
928
- | [`@grantex/autogen`](https://www.npmjs.com/package/@grantex/autogen) | AutoGen integration |
929
- | [`@grantex/vercel-ai`](https://www.npmjs.com/package/@grantex/vercel-ai) | Vercel AI SDK integration |
930
- | [`grantex-crewai`](https://pypi.org/project/grantex-crewai/) | CrewAI integration |
931
- | [`grantex-openai-agents`](https://pypi.org/project/grantex-openai-agents/) | OpenAI Agents SDK integration |
932
- | [`grantex-adk`](https://pypi.org/project/grantex-adk/) | Google ADK integration |
933
- | [`@grantex/mcp`](https://www.npmjs.com/package/@grantex/mcp) | MCP server for Claude Desktop / Cursor / Windsurf |
934
- | [`@grantex/cli`](https://www.npmjs.com/package/@grantex/cli) | Command-line tool |
935
-
936
- ## License
937
-
938
- [Apache 2.0](https://github.com/mishrasanjeev/grantex/blob/main/LICENSE)
939
-
940
- ## Ownership
941
-
942
- Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).
1
+ # @grantex/sdk
2
+
3
+ TypeScript SDK for the [Grantex](https://grantex.dev) delegated authorization protocol — OAuth 2.0 for AI agents.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@grantex/sdk)](https://www.npmjs.com/package/@grantex/sdk)
6
+ [![License](https://img.shields.io/npm/l/@grantex/sdk)](https://github.com/mishrasanjeev/grantex/blob/main/LICENSE)
7
+
8
+ > **[Homepage](https://grantex.dev)** | **[Docs](https://docs.grantex.dev)** | **[API Reference](https://docs.grantex.dev/api-reference)** | **[Sign Up Free](https://grantex.dev/dashboard/signup)** | **[GitHub](https://github.com/mishrasanjeev/grantex)**
9
+
10
+ ## Installation
11
+
12
+ **New in 0.6.0:** the wallet clients preserve the
13
+ server's Base USDC `evmPayment` payload and expose `reconcileReservation()` for
14
+ finalized settlement/expiry. Use `@grantex/x402@0.4.0` or later for
15
+ the automatic 402/payment/retry flow. See [Base custody setup](https://docs.grantex.dev/guides/base-usdc-custody).
16
+
17
+ ```bash
18
+ npm install @grantex/sdk@0.6.0
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { Grantex, verifyGrantToken } from '@grantex/sdk';
25
+
26
+ const grantex = new Grantex({ apiKey: 'YOUR_API_KEY' });
27
+
28
+ // 1. Register an agent
29
+ const agent = await grantex.agents.register({
30
+ name: 'Email Assistant',
31
+ description: 'Reads and sends email on behalf of users',
32
+ scopes: ['email:read', 'email:send'],
33
+ });
34
+
35
+ // 2. Request authorization
36
+ const { consentUrl } = await grantex.authorize({
37
+ agentId: agent.id,
38
+ userId: 'usr_01J...',
39
+ scopes: ['email:read', 'email:send'],
40
+ });
41
+ // Redirect the user to consentUrl — they approve in plain language
42
+
43
+ // 3. Exchange authorization code for a grant token
44
+ // (your redirect callback receives the `code` after user approves)
45
+ const token = await grantex.tokens.exchange({ code, agentId: agent.id });
46
+ console.log(token.grantToken); // RS256-signed JWT
47
+ console.log(token.scopes); // ['email:read', 'email:send']
48
+ console.log(token.grantId); // 'grnt_01J...'
49
+
50
+ // 4. Verify locally using keys retrieved from the issuer's JWKS
51
+ const grant = await verifyGrantToken(token.grantToken, {
52
+ jwksUri: 'https://api.grantex.dev/.well-known/jwks.json',
53
+ });
54
+ console.log(grant.principalId); // 'usr_01J...'
55
+
56
+ // 5. Revoke when done
57
+ await grantex.tokens.revoke(grant.tokenId);
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ ```typescript
63
+ const grantex = new Grantex({
64
+ apiKey: 'gx_....', // or set GRANTEX_API_KEY env var
65
+ baseUrl: 'https://api.grantex.dev', // default
66
+ issuer: 'https://grantex.dev', // optional when token issuer differs from API host
67
+ jwksUri: 'https://api.grantex.dev/.well-known/jwks.json', // optional JWKS override
68
+ timeout: 30000, // request timeout in ms (default: 30s)
69
+ });
70
+ ```
71
+
72
+ | Option | Type | Default | Description |
73
+ |--------|------|---------|-------------|
74
+ | `apiKey` | `string` | `process.env.GRANTEX_API_KEY` | API key for authentication |
75
+ | `baseUrl` | `string` | `https://api.grantex.dev` | Base URL of the Grantex API |
76
+ | `issuer` | `string` | derived from `jwksUri` | Expected JWT issuer for local signature verification |
77
+ | `jwksUri` | `string` | `${baseUrl}/.well-known/jwks.json` | URL from which the verifier retrieves signing keys |
78
+ | `timeout` | `number` | `30000` | Request timeout in milliseconds |
79
+
80
+ ## OAuth Agent Profile Client
81
+
82
+ `OAuthAgentClient` implements the client role in the prepared
83
+ `draft-mishra-oauth-agent-grants-03` profile. It discovers and validates RFC
84
+ 8414 metadata, creates an ES256 DPoP key by default, uses PAR and PKCE S256,
85
+ validates `state` and the RFC 9207 `iss` response parameter, rotates refresh
86
+ tokens, performs same-resource RFC 8693 attenuation, and creates DPoP proofs
87
+ for protected-resource requests.
88
+
89
+ ```typescript
90
+ import { OAuthAgentClient } from '@grantex/sdk';
91
+
92
+ const client = await OAuthAgentClient.create({
93
+ issuer: 'https://grantex.dev',
94
+ clientId: 'ag_01J...',
95
+ redirectUri: 'https://agent.example/callback',
96
+ resource: 'https://api.example/resource',
97
+ });
98
+
99
+ const pending = await client.beginAuthorization({
100
+ scopes: ['grantex.resource.read'],
101
+ principalHint: 'principal@example.com',
102
+ });
103
+
104
+ // Redirect the Principal to pending.authorizationUrl. In the callback:
105
+ const tokens = await client.completeAuthorization(callbackUrl);
106
+
107
+ const response = await client.fetch(
108
+ 'https://api.example/resource',
109
+ tokens.access_token,
110
+ );
111
+
112
+ const narrower = await client.attenuate(tokens.access_token, [
113
+ 'grantex.resource.read',
114
+ ]);
115
+ const refreshAttempt = crypto.randomUUID();
116
+ const rotated = await client.refresh(tokens.refresh_token!, {
117
+ idempotencyKey: refreshAttempt,
118
+ });
119
+ await client.revoke(rotated.refresh_token!, 'refresh_token');
120
+ ```
121
+
122
+ Persist the idempotency key with the old refresh token when lost-response
123
+ recovery must survive a caller restart. Repeating both within 300 seconds uses
124
+ a fresh DPoP proof and returns the exact committed token values with a
125
+ recalculated, non-extended `expires_in`. A
126
+ different key or DPoP identity is treated as refresh-token reuse and revokes
127
+ the token family. When no key is supplied, the client generates and retains one
128
+ for the old refresh token for five minutes in the current process.
129
+
130
+ Persist the generated key securely if an instance must survive process
131
+ restarts. Supply the matching `privateKey` and `publicJwk` to `create`; both are
132
+ required together. `principalHint` is optional account-discovery input and is
133
+ not proof of the Principal's identity; live approval still requires the
134
+ authorization server's passkey authentication. Plain HTTP endpoints are rejected unless
135
+ `allowInsecureLoopback` is enabled for local loopback testing. Revision `-02`
136
+ of the draft family is published as an active individual Internet-Draft;
137
+ revision `-03` is the working candidate implemented here. Neither is an
138
+ IETF-endorsed or independently certified standard.
139
+
140
+ ## Agent prepaid wallets (SDK 0.5+)
141
+
142
+ `PrepaidWalletAgentClient` uses an `OAuthAgentClient` and DPoP access token to
143
+ list assigned wallets, reserve payments, and request threshold reloads.
144
+ `PrincipalPrepaidWalletClient` uses a short-lived principal-session token to
145
+ create and fund wallets, assign safe-default policy, manage layered spend
146
+ policies and exact payment approvals, approve reloads, inspect activity, and
147
+ block an assignment, wallet, or all wallets for one agent.
148
+ `DeveloperPrepaidWalletPolicyClient` manages tenant-level policy with the
149
+ developer API key.
150
+
151
+ The access token must include `wallet:spend` and each action scope used in a
152
+ payment (for example `weather:read`). Agent wallet listings intentionally omit
153
+ custody-provider IDs, wallet addresses, principal IDs, and wallet metadata.
154
+
155
+ ```typescript
156
+ import {
157
+ PrepaidWalletAgentClient,
158
+ PrincipalPrepaidWalletClient,
159
+ } from '@grantex/sdk';
160
+
161
+ const agentWallets = new PrepaidWalletAgentClient({
162
+ oauthClient,
163
+ accessToken,
164
+ });
165
+
166
+ const principalWallets = new PrincipalPrepaidWalletClient({
167
+ baseUrl: 'https://api.grantex.dev',
168
+ sessionToken,
169
+ });
170
+
171
+ await principalWallets.createSpendPolicy({
172
+ name: 'Shared research budget',
173
+ scopeType: 'group',
174
+ scopeId: 'research-agents',
175
+ effect: 'limit',
176
+ maxAmount: '1000000',
177
+ windowType: 'month',
178
+ onExceed: 'require_approval',
179
+ purposes: ['research'],
180
+ });
181
+
182
+ const authorization = await agentWallets.authorizePayment({
183
+ amount: '1000',
184
+ asset: 'USDC',
185
+ network: 'grantex:prepaid',
186
+ recipient: 'merchant:weather-api',
187
+ resource: 'https://merchant.example/weather',
188
+ scope: 'weather:read',
189
+ merchantId: 'merchant:weather-api',
190
+ purpose: 'research',
191
+ projectId: 'climate-2026',
192
+ costCenter: 'engineering',
193
+ maxTimeoutSeconds: 120,
194
+ idempotencyKey: crypto.randomUUID(),
195
+ });
196
+ ```
197
+
198
+ `authorizePayment` returns either a signed reservation or an
199
+ `approval_required` response. After the principal approves that exact request,
200
+ retry with its `approvalRequestId`, the same wallet, idempotency key, and all
201
+ original payment fields. Approval is short-lived and single-use.
202
+
203
+ Amounts are atomic-unit integer strings. Layered policy and exact approval are
204
+ available in `@grantex/sdk` 0.5.0 and later.
205
+
206
+ Self-hosted wallet deployments must also provide correct public resource
207
+ routing, migrations `091` and `092`, durable notification delivery, merchant-side
208
+ idempotency, and any external custody/provider integration. See [Prepaid Wallet
209
+ Production
210
+ Readiness](https://docs.grantex.dev/guides/prepaid-wallet-production);
211
+ installing the SDK alone does not provide those dependencies.
212
+
213
+ ## Commerce V1 / OACP
214
+
215
+ The SDK includes a `commerce` resource for the Grantex Commerce V1 control
216
+ plane and OACP live-pilot flow.
217
+
218
+ ```typescript
219
+ import { Grantex } from '@grantex/sdk';
220
+
221
+ const grantex = new Grantex({ apiKey: process.env.GRANTEX_API_KEY! });
222
+
223
+ // Public merchant publishing profile
224
+ const profile = await grantex.commerce.getProfile({
225
+ merchantId: 'mch_shopify_mgx0n6_22',
226
+ });
227
+ console.log(profile.merchant?.merchant_id);
228
+
229
+ // Catalog grounding
230
+ const catalog = await grantex.commerce.searchCatalog({
231
+ merchant_id: 'mch_shopify_mgx0n6_22',
232
+ query: 'shirt',
233
+ limit: 5,
234
+ });
235
+
236
+ // Agent cart creation. Commerce write paths require Idempotency-Key.
237
+ const cart = await grantex.commerce.createCart({
238
+ idempotencyKey: crypto.randomUUID(),
239
+ merchant_id: 'mch_shopify_mgx0n6_22',
240
+ currency: 'INR',
241
+ line_items: [
242
+ { variant_id: String(catalog.items[0]?.['variant_id']), quantity: 1 },
243
+ ],
244
+ });
245
+
246
+ // Consent request and Commerce Passport exchange
247
+ const consent = await grantex.commerce.createConsentRequest({
248
+ merchant_id: 'mch_shopify_mgx0n6_22',
249
+ passport_type: 'checkout',
250
+ max_amount: Number(cart.data['total_amount']),
251
+ currency: 'INR',
252
+ });
253
+
254
+ // Redirect the buyer to consent.data['consent_url'], then exchange after
255
+ // the consent request is granted.
256
+ const passport = await grantex.commerce.exchangeConsentForPassport({
257
+ consent_request_id: String(consent.data['consent_request_id']),
258
+ });
259
+
260
+ const payment = await grantex.commerce.createPaymentIntent({
261
+ idempotencyKey: crypto.randomUUID(),
262
+ merchant_id: 'mch_shopify_mgx0n6_22',
263
+ cart_id: String(cart.data['cart_id']),
264
+ passport_jwt: String(passport.data['passport_jwt']),
265
+ amount_minor_units: Number(cart.data['total_amount']),
266
+ currency: 'INR',
267
+ provider_key: 'plural',
268
+ });
269
+
270
+ const checkout = await grantex.commerce.createCheckoutLink(
271
+ String(payment.data['payment_intent_id']),
272
+ {
273
+ idempotencyKey: crypto.randomUUID(),
274
+ passport_jwt: String(passport.data['passport_jwt']),
275
+ success_url: 'https://buyer.example/success',
276
+ cancel_url: 'https://buyer.example/cancel',
277
+ },
278
+ );
279
+ ```
280
+
281
+ Plural webhook intake is available at
282
+ `https://api.grantex.dev/v1/webhooks/providers/plural`. Provider webhooks are
283
+ normally called by the provider dashboard, not by application code. The SDK
284
+ also exposes `grantex.commerce.getOpsHealth()` and
285
+ `grantex.commerce.listProviderWebhookEvents()` for operator health checks.
286
+
287
+ ## PKCE Support
288
+
289
+ The SDK includes built-in PKCE (Proof Key for Code Exchange) support using the S256 method for secure authorization flows:
290
+
291
+ ```typescript
292
+ import { Grantex, generatePkce } from '@grantex/sdk';
293
+
294
+ const grantex = new Grantex({ apiKey: 'YOUR_API_KEY' });
295
+
296
+ // 1. Generate a PKCE challenge
297
+ const pkce = generatePkce();
298
+ // pkce.codeVerifier — random 43-char string (keep secret)
299
+ // pkce.codeChallenge — SHA-256 hash of verifier (send to server)
300
+ // pkce.codeChallengeMethod — 'S256'
301
+
302
+ // 2. Pass the challenge when requesting authorization
303
+ const { consentUrl } = await grantex.authorize({
304
+ agentId: 'ag_01J...',
305
+ userId: 'usr_01J...',
306
+ scopes: ['files:read'],
307
+ codeChallenge: pkce.codeChallenge,
308
+ codeChallengeMethod: pkce.codeChallengeMethod,
309
+ });
310
+
311
+ // 3. Exchange the code with the verifier
312
+ const token = await grantex.tokens.exchange({
313
+ code: 'auth_code_from_redirect',
314
+ agentId: 'ag_01J...',
315
+ codeVerifier: pkce.codeVerifier,
316
+ });
317
+ ```
318
+
319
+ ---
320
+
321
+ ## API Reference
322
+
323
+ ### Authorization
324
+
325
+ #### `grantex.authorize(params)`
326
+
327
+ Initiate the delegated authorization flow. Returns a consent URL to redirect the user to.
328
+
329
+ ```typescript
330
+ const request = await grantex.authorize({
331
+ agentId: 'ag_01J...',
332
+ userId: 'usr_01J...',
333
+ scopes: ['files:read', 'email:send'],
334
+ audience: 'https://api.example.com', // optional; becomes the JWT aud claim
335
+ expiresIn: '24h', // optional
336
+ redirectUri: 'https://...' // optional
337
+ });
338
+
339
+ console.log(request.consentUrl); // redirect user here
340
+ console.log(request.authRequestId); // track the request
341
+ console.log(request.expiresAt); // ISO 8601 timestamp
342
+ ```
343
+
344
+ **Returns**: `AuthorizationRequest`
345
+
346
+ | Field | Type | Description |
347
+ |-------|------|-------------|
348
+ | `authRequestId` | `string` | Unique ID for this authorization request |
349
+ | `consentUrl` | `string` | URL to redirect the user to for consent |
350
+ | `agentId` | `string` | The agent requesting authorization |
351
+ | `principalId` | `string` | The user being asked for consent |
352
+ | `scopes` | `string[]` | Requested scopes |
353
+ | `expiresAt` | `string` | When the request expires (ISO 8601) |
354
+ | `status` | `string` | `'pending'`, `'approved'`, `'denied'`, or `'expired'` |
355
+
356
+ ---
357
+
358
+ ### Agents
359
+
360
+ #### `grantex.agents.register(params)`
361
+
362
+ Register a new AI agent.
363
+
364
+ ```typescript
365
+ const agent = await grantex.agents.register({
366
+ name: 'Code Review Bot',
367
+ description: 'Reviews pull requests and suggests improvements',
368
+ scopes: ['repo:read', 'pr:comment'],
369
+ });
370
+ ```
371
+
372
+ #### `grantex.agents.get(agentId)`
373
+
374
+ ```typescript
375
+ const agent = await grantex.agents.get('ag_01J...');
376
+ ```
377
+
378
+ #### `grantex.agents.list()`
379
+
380
+ ```typescript
381
+ const { agents } = await grantex.agents.list();
382
+ ```
383
+
384
+ #### `grantex.agents.update(agentId, params)`
385
+
386
+ ```typescript
387
+ const agent = await grantex.agents.update('ag_01J...', {
388
+ name: 'Updated Name',
389
+ scopes: ['repo:read', 'pr:comment', 'pr:approve'],
390
+ });
391
+ ```
392
+
393
+ #### `grantex.agents.delete(agentId)`
394
+
395
+ ```typescript
396
+ await grantex.agents.delete('ag_01J...');
397
+ ```
398
+
399
+ ---
400
+
401
+ ### Grants
402
+
403
+ #### `grantex.grants.get(grantId)`
404
+
405
+ ```typescript
406
+ const grant = await grantex.grants.get('grnt_01J...');
407
+ ```
408
+
409
+ #### `grantex.grants.list(params?)`
410
+
411
+ ```typescript
412
+ const { grants } = await grantex.grants.list({
413
+ agentId: 'ag_01J...', // optional filter
414
+ principalId: 'usr_01J...', // optional filter
415
+ status: 'active', // 'active' | 'revoked' | 'expired'
416
+ page: 1,
417
+ pageSize: 20,
418
+ });
419
+ ```
420
+
421
+ #### `grantex.grants.revoke(grantId)`
422
+
423
+ ```typescript
424
+ await grantex.grants.revoke('grnt_01J...');
425
+ ```
426
+
427
+ #### `grantex.grants.delegate(params)`
428
+
429
+ Create a delegated sub-agent grant (per [SPEC Section 9](https://github.com/mishrasanjeev/grantex/blob/main/SPEC.md)).
430
+
431
+ ```typescript
432
+ const delegation = await grantex.grants.delegate({
433
+ parentGrantToken: 'eyJhbG...',
434
+ subAgentId: 'ag_02K...',
435
+ scopes: ['files:read'], // must be subset of parent scopes
436
+ expiresIn: '1h', // optional, cannot exceed parent
437
+ });
438
+
439
+ console.log(delegation.grantToken); // new JWT for the sub-agent
440
+ console.log(delegation.grantId);
441
+ ```
442
+
443
+ #### `grantex.grants.verify(token)`
444
+
445
+ Verify a grant token via the API (online verification with real-time revocation check).
446
+
447
+ ```typescript
448
+ const verified = await grantex.grants.verify('eyJhbG...');
449
+ console.log(verified.principalId);
450
+ console.log(verified.scopes);
451
+ ```
452
+
453
+ Throws `GrantexTokenError` when the token is inactive, revoked, expired, or otherwise unusable.
454
+
455
+ ---
456
+
457
+ ### Tokens
458
+
459
+ #### `grantex.tokens.exchange(params)`
460
+
461
+ Exchange an authorization code for a grant token. This is the standard way to obtain a grant token after the user approves the consent request.
462
+
463
+ ```typescript
464
+ const token = await grantex.tokens.exchange({
465
+ code: 'auth_code_from_redirect', // from your redirect callback
466
+ agentId: 'ag_01J...',
467
+ });
468
+
469
+ console.log(token.grantToken); // RS256-signed JWT — pass this to your agent
470
+ console.log(token.grantId); // grant record ID
471
+ console.log(token.scopes); // granted scopes
472
+ console.log(token.expiresAt); // ISO 8601 expiry
473
+ console.log(token.refreshToken); // for token refresh
474
+ ```
475
+
476
+ **Returns**: `ExchangeTokenResponse`
477
+
478
+ | Field | Type | Description |
479
+ |-------|------|-------------|
480
+ | `grantToken` | `string` | Signed RS256 JWT — the agent's bearer credential |
481
+ | `grantId` | `string` | Grant record ID |
482
+ | `scopes` | `string[]` | Scopes the user approved |
483
+ | `expiresAt` | `string` | Underlying grant expiry (ISO 8601) |
484
+ | `refreshToken` | `string` | Refresh token for rotating credentials while the grant remains active |
485
+
486
+ Refresh tokens are single-use and rotate on every accepted refresh. If a response is lost after commit, retry the same previous refresh token and idempotency key. The SDK retains an omitted key for five minutes in the current process; persist an explicit `idempotencyKey` with the old token when recovery must survive restart or failover. Grantex returns the already-rotated pair without extending `expiresAt`; after grant expiry, re-authorize.
487
+
488
+ ---
489
+
490
+ #### `grantex.tokens.verify(token)`
491
+
492
+ Online token verification with revocation status.
493
+
494
+ ```typescript
495
+ const result = await grantex.tokens.verify('eyJhbG...');
496
+ if (result.valid) {
497
+ console.log(result.scopes); // ['files:read']
498
+ console.log(result.principal); // 'usr_01J...'
499
+ console.log(result.agent); // 'ag_01J...'
500
+ console.log(result.grantId);
501
+ console.log(result.expiresAt);
502
+ }
503
+ ```
504
+
505
+ #### `grantex.tokens.revoke(tokenId)`
506
+
507
+ Revoke a token by its JTI. Blocklisted in Redis immediately; all sub-delegated tokens are also invalidated.
508
+
509
+ ```typescript
510
+ await grantex.tokens.revoke('tok_01J...');
511
+ ```
512
+
513
+ ---
514
+
515
+ ### Local Token Verification
516
+
517
+ #### `verifyGrantToken(token, options)`
518
+
519
+ Verify a grant token locally with RS256 signing keys retrieved from the published JWKS URL. A bounded process-level resolver cache reuses valid keys for each normalized JWKS URI; initial retrieval and key-rotation refreshes may require network access.
520
+
521
+ ```typescript
522
+ import { verifyGrantToken } from '@grantex/sdk';
523
+
524
+ const grant = await verifyGrantToken('eyJhbG...', {
525
+ jwksUri: 'https://api.grantex.dev/.well-known/jwks.json',
526
+ issuer: 'https://grantex.dev', // optional when issuer differs from JWKS host
527
+ requiredScopes: ['files:read'], // optional — rejects if missing
528
+ audience: 'https://myapp.com', // optional — validates aud claim
529
+ });
530
+ ```
531
+
532
+ If you call a deployment through a raw Cloud Run URL or another internal host, but the service signs tokens for a canonical public domain, pass `issuer` explicitly. Otherwise issuer validation will reject a valid token because the JWT `iss` claim will not match the transport host.
533
+
534
+ **Returns**: `VerifiedGrant`
535
+
536
+ | Field | Type | Description |
537
+ |-------|------|-------------|
538
+ | `tokenId` | `string` | Unique token ID (JWT `jti` claim) |
539
+ | `grantId` | `string` | Grant record ID |
540
+ | `principalId` | `string` | User who authorized the grant (`sub` claim) |
541
+ | `agentDid` | `string` | Agent's DID (`agt` claim) |
542
+ | `developerId` | `string` | Developer org ID (`dev` claim) |
543
+ | `scopes` | `string[]` | Granted scopes (`scp` claim) |
544
+ | `issuedAt` | `number` | Issued-at timestamp (seconds since epoch) |
545
+ | `expiresAt` | `number` | Expiry timestamp (seconds since epoch) |
546
+ | `parentAgentDid` | `string?` | Parent agent DID (delegation only) |
547
+ | `parentGrantId` | `string?` | Parent grant ID (delegation only) |
548
+ | `delegationDepth` | `number?` | Delegation depth (0 = root) |
549
+
550
+ ---
551
+
552
+ ### Audit
553
+
554
+ #### `grantex.audit.log(params)`
555
+
556
+ Log an auditable action taken by an agent.
557
+
558
+ ```typescript
559
+ const entry = await grantex.audit.log({
560
+ agentId: 'ag_01J...',
561
+ agentDid: 'did:grantex:ag_01J...',
562
+ grantId: 'grnt_01J...',
563
+ principalId: 'usr_01J...',
564
+ action: 'email:send',
565
+ metadata: { to: 'user@example.com', subject: 'Hello' },
566
+ status: 'success', // 'success' | 'failure' | 'blocked'
567
+ });
568
+ ```
569
+
570
+ #### `grantex.audit.list(params?)`
571
+
572
+ ```typescript
573
+ const { entries } = await grantex.audit.list({
574
+ agentId: 'ag_01J...',
575
+ action: 'email:send',
576
+ since: '2026-01-01T00:00:00Z',
577
+ until: '2026-02-28T23:59:59Z',
578
+ page: 1,
579
+ pageSize: 50,
580
+ });
581
+ ```
582
+
583
+ #### `grantex.audit.get(entryId)`
584
+
585
+ ```typescript
586
+ const entry = await grantex.audit.get('alog_01J...');
587
+ console.log(entry.hash); // SHA-256 hash for tamper evidence
588
+ console.log(entry.prevHash); // previous entry hash (chain integrity)
589
+ ```
590
+
591
+ ---
592
+
593
+ ### Webhooks
594
+
595
+ #### `grantex.webhooks.create(params)`
596
+
597
+ ```typescript
598
+ const webhook = await grantex.webhooks.create({
599
+ url: 'https://myapp.com/webhooks/grantex',
600
+ events: ['grant.created', 'grant.revoked', 'token.issued'],
601
+ });
602
+ console.log(webhook.secret); // HMAC secret for signature verification
603
+ ```
604
+
605
+ #### `grantex.webhooks.list()`
606
+
607
+ ```typescript
608
+ const { webhooks } = await grantex.webhooks.list();
609
+ ```
610
+
611
+ #### `grantex.webhooks.delete(webhookId)`
612
+
613
+ ```typescript
614
+ await grantex.webhooks.delete('wh_01J...');
615
+ ```
616
+
617
+ #### Webhook Signature Verification
618
+
619
+ ```typescript
620
+ import { verifyWebhookSignature } from '@grantex/sdk';
621
+
622
+ // In your webhook handler
623
+ verifyWebhookSignature(requestBody, signatureHeader, webhookSecret);
624
+ ```
625
+
626
+ ---
627
+
628
+ ### Policies
629
+
630
+ Define fine-grained access control rules for agents.
631
+
632
+ #### `grantex.policies.create(params)`
633
+
634
+ ```typescript
635
+ const policy = await grantex.policies.create({
636
+ name: 'Block after hours',
637
+ effect: 'deny',
638
+ priority: 10,
639
+ scopes: ['email:send'],
640
+ timeOfDayStart: '18:00',
641
+ timeOfDayEnd: '08:00',
642
+ });
643
+ ```
644
+
645
+ #### `grantex.policies.list()`
646
+
647
+ ```typescript
648
+ const { policies, total } = await grantex.policies.list();
649
+ ```
650
+
651
+ #### `grantex.policies.get(policyId)` / `update(policyId, params)` / `delete(policyId)`
652
+
653
+ ```typescript
654
+ const policy = await grantex.policies.get('pol_01J...');
655
+
656
+ await grantex.policies.update('pol_01J...', { effect: 'allow' });
657
+
658
+ await grantex.policies.delete('pol_01J...');
659
+ ```
660
+
661
+ ---
662
+
663
+ ### Compliance
664
+
665
+ #### `grantex.compliance.getSummary(params?)`
666
+
667
+ ```typescript
668
+ const summary = await grantex.compliance.getSummary({
669
+ since: '2026-01-01T00:00:00Z',
670
+ until: '2026-02-28T23:59:59Z',
671
+ });
672
+ console.log(summary.agents); // { total, active, suspended, revoked }
673
+ console.log(summary.grants); // { total, active, revoked, expired }
674
+ console.log(summary.auditEntries); // { total, success, failure, blocked }
675
+ ```
676
+
677
+ #### `grantex.compliance.exportGrants(params?)`
678
+
679
+ ```typescript
680
+ const { grants, total } = await grantex.compliance.exportGrants({
681
+ status: 'active',
682
+ });
683
+ ```
684
+
685
+ #### `grantex.compliance.exportAudit(params?)`
686
+
687
+ ```typescript
688
+ const { entries, total } = await grantex.compliance.exportAudit({
689
+ since: '2026-01-01T00:00:00Z',
690
+ agentId: 'ag_01J...',
691
+ });
692
+ ```
693
+
694
+ #### `grantex.compliance.evidencePack(params?)`
695
+
696
+ Generate a full SOC 2 / GDPR evidence pack with audit chain integrity verification.
697
+
698
+ ```typescript
699
+ const pack = await grantex.compliance.evidencePack({
700
+ framework: 'soc2', // 'soc2' | 'gdpr' | 'all'
701
+ since: '2026-01-01T00:00:00Z',
702
+ });
703
+
704
+ console.log(pack.chainIntegrity.valid); // true
705
+ console.log(pack.chainIntegrity.checkedEntries); // 1042
706
+ console.log(pack.summary);
707
+ console.log(pack.grants);
708
+ console.log(pack.auditEntries);
709
+ console.log(pack.policies);
710
+ ```
711
+
712
+ ---
713
+
714
+ ### Anomaly Detection
715
+
716
+ #### `grantex.anomalies.detect()`
717
+
718
+ Run anomaly detection across all agents.
719
+
720
+ ```typescript
721
+ const { anomalies, total } = await grantex.anomalies.detect();
722
+ // anomaly types: 'rate_spike' | 'high_failure_rate' | 'new_principal' | 'off_hours_activity'
723
+ ```
724
+
725
+ #### `grantex.anomalies.list(params?)`
726
+
727
+ ```typescript
728
+ const { anomalies } = await grantex.anomalies.list({
729
+ unacknowledged: true, // only open anomalies
730
+ });
731
+ ```
732
+
733
+ #### `grantex.anomalies.acknowledge(anomalyId)`
734
+
735
+ ```typescript
736
+ const anomaly = await grantex.anomalies.acknowledge('anom_01J...');
737
+ ```
738
+
739
+ ---
740
+
741
+ ### Billing
742
+
743
+ #### `grantex.billing.getSubscription()`
744
+
745
+ ```typescript
746
+ const sub = await grantex.billing.getSubscription();
747
+ console.log(sub.plan); // 'free' | 'pro' | 'enterprise'
748
+ console.log(sub.status); // 'active' | 'past_due' | 'canceled'
749
+ console.log(sub.currentPeriodEnd); // ISO 8601 or null
750
+ ```
751
+
752
+ #### `grantex.billing.createCheckout(params)`
753
+
754
+ ```typescript
755
+ const { checkoutUrl } = await grantex.billing.createCheckout({
756
+ plan: 'pro',
757
+ successUrl: 'https://myapp.com/billing/success',
758
+ cancelUrl: 'https://myapp.com/billing/cancel',
759
+ });
760
+ // Redirect user to checkoutUrl
761
+ ```
762
+
763
+ #### `grantex.billing.createPortal(params)`
764
+
765
+ ```typescript
766
+ const { portalUrl } = await grantex.billing.createPortal({
767
+ returnUrl: 'https://myapp.com/settings',
768
+ });
769
+ ```
770
+
771
+ ---
772
+
773
+ ### SCIM 2.0 Provisioning
774
+
775
+ Sync users from your identity provider.
776
+
777
+ #### Token Management
778
+
779
+ ```typescript
780
+ // Create a SCIM bearer token
781
+ const { token, id, label } = await grantex.scim.createToken({
782
+ label: 'Okta SCIM integration',
783
+ });
784
+ // token is returned once — store it securely
785
+
786
+ const { tokens } = await grantex.scim.listTokens();
787
+
788
+ await grantex.scim.revokeToken('scimtok_01J...');
789
+ ```
790
+
791
+ #### User Operations
792
+
793
+ ```typescript
794
+ // List provisioned users
795
+ const { Resources, totalResults } = await grantex.scim.listUsers({
796
+ startIndex: 1,
797
+ count: 100,
798
+ });
799
+
800
+ // Create a user
801
+ const user = await grantex.scim.createUser({
802
+ userName: 'alice@example.com',
803
+ displayName: 'Alice',
804
+ emails: [{ value: 'alice@example.com', primary: true }],
805
+ });
806
+
807
+ // Get / Replace / Patch / Delete
808
+ const user = await grantex.scim.getUser('scimusr_01J...');
809
+
810
+ await grantex.scim.replaceUser('scimusr_01J...', { userName: 'alice@new.com' });
811
+
812
+ await grantex.scim.updateUser('scimusr_01J...', [
813
+ { op: 'replace', path: 'active', value: false },
814
+ ]);
815
+
816
+ await grantex.scim.deleteUser('scimusr_01J...');
817
+ ```
818
+
819
+ ---
820
+
821
+ ### SSO (OIDC)
822
+
823
+ #### `grantex.sso.createConfig(params)`
824
+
825
+ ```typescript
826
+ const config = await grantex.sso.createConfig({
827
+ issuerUrl: 'https://accounts.google.com',
828
+ clientId: 'xxx.apps.googleusercontent.com',
829
+ clientSecret: 'GOCSPX-...',
830
+ redirectUri: 'https://myapp.com/auth/callback',
831
+ });
832
+ ```
833
+
834
+ #### `grantex.sso.getConfig()` / `deleteConfig()`
835
+
836
+ ```typescript
837
+ const config = await grantex.sso.getConfig();
838
+ await grantex.sso.deleteConfig();
839
+ ```
840
+
841
+ #### `grantex.sso.getLoginUrl(org)`
842
+
843
+ ```typescript
844
+ const { authorizeUrl } = await grantex.sso.getLoginUrl('dev_01J...');
845
+ // Redirect user to authorizeUrl
846
+ ```
847
+
848
+ #### `grantex.sso.handleCallback(code, state)`
849
+
850
+ ```typescript
851
+ const { email, name, sub, developerId } = await grantex.sso.handleCallback(code, state);
852
+ ```
853
+
854
+ ---
855
+
856
+ ## Error Handling
857
+
858
+ All errors extend `GrantexError`:
859
+
860
+ ```typescript
861
+ import {
862
+ GrantexError, // base class
863
+ GrantexApiError, // API returned an error (has statusCode, body, requestId)
864
+ GrantexAuthError, // 401/403 — invalid or missing API key
865
+ GrantexTokenError, // token verification failed (invalid signature, expired, etc.)
866
+ GrantexNetworkError, // network failure (timeout, DNS, connection refused)
867
+ } from '@grantex/sdk';
868
+
869
+ try {
870
+ await grantex.agents.get('ag_invalid');
871
+ } catch (err) {
872
+ if (err instanceof GrantexAuthError) {
873
+ console.error('Auth failed:', err.statusCode); // 401 or 403
874
+ console.error('Request ID:', err.requestId);
875
+ } else if (err instanceof GrantexApiError) {
876
+ console.error('API error:', err.statusCode, err.body);
877
+ } else if (err instanceof GrantexNetworkError) {
878
+ console.error('Network error:', err.message, err.cause);
879
+ }
880
+ }
881
+ ```
882
+
883
+ ---
884
+
885
+ ## Requirements
886
+
887
+ - Node.js 18+
888
+ - ESM (`"type": "module"` in your package.json, or use dynamic `import()`)
889
+
890
+ ## Links
891
+
892
+ - [GitHub](https://github.com/mishrasanjeev/grantex)
893
+ - [Protocol Specification](https://github.com/mishrasanjeev/grantex/blob/main/SPEC.md)
894
+ - [Python SDK](https://pypi.org/project/grantex/)
895
+ - [API Reference](https://api.grantex.dev/.well-known/jwks.json)
896
+
897
+ ## Scope Enforcement (v0.3.1)
898
+
899
+ Enforce tool-level permissions on **any connector** — define your own manifests or use the 53 pre-built ones.
900
+
901
+ ```typescript
902
+ import { Grantex, ToolManifest, Permission } from '@grantex/sdk';
903
+
904
+ const grantex = new Grantex({ apiKey: 'gx_...' });
905
+
906
+ // Define a manifest for any connector — no dependency on Grantex to add support
907
+ grantex.loadManifest(new ToolManifest({
908
+ connector: 'my-crm',
909
+ tools: { search: Permission.READ, create_deal: Permission.WRITE, delete_account: Permission.DELETE },
910
+ }));
911
+
912
+ const result = await grantex.enforce({ grantToken: token, connector: 'my-crm', tool: 'delete_account' });
913
+ // result.allowed = false — "write scope does not permit delete operations"
914
+ ```
915
+
916
+ **Features:**
917
+ - `enforce()` — verify JWT + check tool permission via manifest, <1ms
918
+ - `wrapTool()` — auto-enforce on LangChain tools
919
+ - `enforceMiddleware()` — Express/Fastify HTTP middleware
920
+ - Define custom manifests for any connector: inline, from JSON, or auto-generated via CLI
921
+ - 53 pre-built manifests included (Salesforce, HubSpot, Jira, Stripe, SAP, S3, and 47 more)
922
+ - Permission hierarchy: `admin > delete > write > read`
923
+ - Permissive mode for migration (`enforceMode: 'permissive'`)
924
+
925
+ [Full Guide](https://docs.grantex.dev/guides/scope-enforcement) | [API Reference](https://docs.grantex.dev/sdks/typescript/enforce)
926
+
927
+ ## Grantex Ecosystem
928
+
929
+ | Package | Description |
930
+ |---|---|
931
+ | [`grantex`](https://pypi.org/project/grantex/) | Python SDK |
932
+ | [`@grantex/langchain`](https://www.npmjs.com/package/@grantex/langchain) | LangChain integration |
933
+ | [`@grantex/autogen`](https://www.npmjs.com/package/@grantex/autogen) | AutoGen integration |
934
+ | [`@grantex/vercel-ai`](https://www.npmjs.com/package/@grantex/vercel-ai) | Vercel AI SDK integration |
935
+ | [`grantex-crewai`](https://pypi.org/project/grantex-crewai/) | CrewAI integration |
936
+ | [`grantex-openai-agents`](https://pypi.org/project/grantex-openai-agents/) | OpenAI Agents SDK integration |
937
+ | [`grantex-adk`](https://pypi.org/project/grantex-adk/) | Google ADK integration |
938
+ | [`@grantex/mcp`](https://www.npmjs.com/package/@grantex/mcp) | MCP server for Claude Desktop / Cursor / Windsurf |
939
+ | [`@grantex/cli`](https://www.npmjs.com/package/@grantex/cli) | Command-line tool |
940
+
941
+ ## License
942
+
943
+ [Apache 2.0](https://github.com/mishrasanjeev/grantex/blob/main/LICENSE)
944
+
945
+ ## Ownership
946
+
947
+ Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).