Skip to Content
GuidesEnforce Delegation Constraints

Enforcing Delegation Constraints

Credat stores constraints inside delegation credentials, but enforcement is your responsibility. The SDK embeds constraint data in the signed token — your service reads them after verification and decides whether to allow or deny the operation.

How constraints are stored

When you issue a delegation with constraints, they’re embedded as selectively-disclosable claims in the SD-JWT VC token:

import { delegate, generateKeyPair } from "@credat/sdk"; const ownerKeyPair = generateKeyPair("ES256"); const delegation = await delegate({ agent: "did:web:acme.com:agents:trading-bot", owner: "did:web:acme.com", ownerKeyPair, scopes: ["trades:execute", "portfolio:read"], constraints: { maxTransactionValue: 10_000, allowedDomains: ["api.exchange.com", "api.broker.com"], rateLimit: 50, }, }); console.log(delegation.claims.constraints); // { // maxTransactionValue: 10000, // allowedDomains: ["api.exchange.com", "api.broker.com"], // rateLimit: 50, // }

The constraints travel with the credential — any verifier that receives the token can see what limits the owner imposed.

Extracting constraints after verification

After verifying a delegation, constraints are available on the result:

import { verifyDelegation } from "@credat/sdk"; const result = await verifyDelegation(delegation.token, { ownerPublicKey: ownerKeyPair.publicKey, }); if (!result.valid) { throw new Error(`Verification failed: ${result.errors[0].message}`); } // Constraints may be undefined if the owner didn't set any const constraints = result.constraints ?? {};

Implementing constraint checks

maxTransactionValue

Reject operations that exceed the delegated budget:

function checkTransactionValue( constraints: Record<string, unknown>, amount: number, ): void { const max = constraints.maxTransactionValue as number | undefined; if (max !== undefined && amount > max) { throw new Error( `Transaction value ${amount} exceeds maximum ${max}`, ); } } // Usage checkTransactionValue(constraints, order.totalAmount);

allowedDomains

Restrict which domains the agent can interact with:

function checkAllowedDomain( constraints: Record<string, unknown>, targetDomain: string, ): void { const allowed = constraints.allowedDomains as string[] | undefined; if (allowed && !allowed.includes(targetDomain)) { throw new Error( `Domain "${targetDomain}" not in allowed list: ${allowed.join(", ")}`, ); } } // Usage const url = new URL(request.url); checkAllowedDomain(constraints, url.hostname);

rateLimit with a sliding window

The SDK stores the limit — you track the count:

// Simple in-memory sliding window (use Redis in production) const windows = new Map<string, { count: number; resetAt: number }>(); const WINDOW_MS = 60 * 60 * 1000; // 1 hour function checkRateLimit( constraints: Record<string, unknown>, agentDid: string, ): void { const limit = constraints.rateLimit as number | undefined; if (limit === undefined) return; const now = Date.now(); const window = windows.get(agentDid); if (!window || now > window.resetAt) { windows.set(agentDid, { count: 1, resetAt: now + WINDOW_MS }); return; } if (window.count >= limit) { const retryAfter = Math.ceil((window.resetAt - now) / 1000); throw new Error( `Rate limit exceeded (${limit}/window). Retry after ${retryAfter}s`, ); } window.count++; } // Usage checkRateLimit(constraints, result.agent);

Custom constraints

The DelegationConstraints type has an index signature ([key: string]: unknown), so you can add domain-specific constraints:

const delegation = await delegate({ agent: agent.did, owner: "did:web:acme.com", ownerKeyPair, scopes: ["data:query"], constraints: { "custom:maxRows": 1000, "custom:allowedTables": ["users", "orders"], "custom:readOnly": true, }, });

Enforce them the same way:

function checkCustomConstraints( constraints: Record<string, unknown>, query: { table: string; rowLimit: number; isWrite: boolean }, ): void { const maxRows = constraints["custom:maxRows"] as number | undefined; if (maxRows !== undefined && query.rowLimit > maxRows) { throw new Error(`Row limit ${query.rowLimit} exceeds max ${maxRows}`); } const allowedTables = constraints["custom:allowedTables"] as string[] | undefined; if (allowedTables && !allowedTables.includes(query.table)) { throw new Error(`Table "${query.table}" not allowed`); } const readOnly = constraints["custom:readOnly"] as boolean | undefined; if (readOnly && query.isWrite) { throw new Error("Write operations not permitted"); } }

Using validateConstraints()

The SDK provides a built-in validateConstraints() function that performs all three standard checks (maxTransactionValue, allowedDomains, rateLimit) in one call:

import { validateConstraints } from "@credat/sdk"; const violations = validateConstraints( result.constraints ?? {}, { transactionValue: order.totalAmount, domain: req.hostname, rateLimit: currentRequestCount, }, ); if (violations.length > 0) { for (const v of violations) { console.log(`${v.constraint}: ${v.message}`); } throw new Error("Constraint violated"); }

Each violation includes a constraint name (e.g. "maxTransactionValue") and a human-readable message. For custom constraints beyond the three built-in checks, add your own validation logic after calling validateConstraints().

Putting it all together

A middleware that verifies the delegation and enforces all constraints before handling the request:

import { verifyDelegation, hasScope } from "@credat/sdk"; async function enforceConstraints(req, res, next) { const result = await verifyDelegation(req.delegationToken, { ownerPublicKey: req.ownerPublicKey, }); if (!result.valid) { return res.status(401).json({ error: "Invalid delegation", details: result.errors }); } // 1. Check scopes if (!hasScope(result, req.requiredScope)) { return res.status(403).json({ error: `Missing scope: ${req.requiredScope}` }); } // 2. Enforce constraints (fail-closed: reject if check throws) const constraints = result.constraints ?? {}; try { checkTransactionValue(constraints, req.body?.amount ?? 0); checkAllowedDomain(constraints, req.hostname); checkRateLimit(constraints, result.agent); } catch (err) { return res.status(403).json({ error: "Constraint violated", detail: err.message }); } // 3. Attach verified info req.agent = result; next(); }

Best practices

PracticeWhy
Fail closedIf a constraint check throws or the field is malformed, deny the request. Never skip checks on error.
Log violationsRecord every constraint violation with the agent DID, constraint name, attempted value, and timestamp. You’ll need this for auditing.
Check before actingEnforce constraints before executing the operation, not after. A failed check after a trade executes doesn’t help.
Use custom prefixesNamespace custom constraints with a prefix like custom: or your domain to avoid collisions with future SDK fields.
Store rate-limit state externallyIn-memory counters reset on restart. Use Redis or a database for production rate limiting.
Combine with scopesConstraints refine what scopes allow. Check scopes first (cheap), then constraints (may require state lookups).

Next steps

Last updated on