Claude-code-plugins-plus exa-enterprise-rbac
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/exa-pack/skills/exa-enterprise-rbac" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-exa-enterprise-rbac && rm -rf "$T"
manifest:
plugins/saas-packs/exa-pack/skills/exa-enterprise-rbac/SKILL.mdsource content
Exa Enterprise RBAC
Overview
Manage access to Exa search API through API key scoping and application-level controls. Exa is API-key-based (no built-in RBAC), so access control is implemented through multiple API keys per use case, application-layer permission enforcement, domain restrictions per team, and per-key usage monitoring.
Prerequisites
- Exa API account with team/enterprise plan
- Dashboard access at dashboard.exa.ai
- Multiple API keys for key isolation
Instructions
Step 1: Key-Per-Use-Case Architecture
// config/exa-keys.ts import Exa from "exa-js"; // Create separate clients for each use case const exaClients = { // High-volume RAG pipeline — production key with higher limits ragPipeline: new Exa(process.env.EXA_KEY_RAG!), // Internal research tool — lower volume key researchTool: new Exa(process.env.EXA_KEY_RESEARCH!), // Customer-facing search — separate key for isolation customerSearch: new Exa(process.env.EXA_KEY_CUSTOMER!), }; export function getExaForUseCase( useCase: keyof typeof exaClients ): Exa { const client = exaClients[useCase]; if (!client) throw new Error(`No Exa client for use case: ${useCase}`); return client; }
Step 2: Application-Level Permission Enforcement
// middleware/exa-permissions.ts interface ExaPermissions { maxResults: number; allowedTypes: ("auto" | "neural" | "keyword" | "fast" | "deep")[]; allowedCategories: string[]; includeDomains?: string[]; // restrict to these domains dailySearchLimit: number; } const ROLE_PERMISSIONS: Record<string, ExaPermissions> = { "rag-pipeline": { maxResults: 10, allowedTypes: ["neural", "auto"], allowedCategories: [], dailySearchLimit: 10000, }, "research-analyst": { maxResults: 25, allowedTypes: ["neural", "keyword", "auto", "deep"], allowedCategories: ["research paper", "news"], dailySearchLimit: 500, }, "marketing-team": { maxResults: 5, allowedTypes: ["keyword", "auto"], allowedCategories: ["company", "news"], dailySearchLimit: 100, }, "compliance-team": { maxResults: 10, allowedTypes: ["keyword", "auto"], allowedCategories: [], includeDomains: ["nist.gov", "owasp.org", "sans.org", "sec.gov"], dailySearchLimit: 200, }, }; function validateSearchRequest( role: string, searchType: string, numResults: number, category?: string ): { allowed: boolean; reason?: string } { const perms = ROLE_PERMISSIONS[role]; if (!perms) return { allowed: false, reason: "Unknown role" }; if (!perms.allowedTypes.includes(searchType as any)) { return { allowed: false, reason: `Search type ${searchType} not allowed for ${role}` }; } if (numResults > perms.maxResults) { return { allowed: false, reason: `Max ${perms.maxResults} results for ${role}` }; } if (category && perms.allowedCategories.length > 0 && !perms.allowedCategories.includes(category)) { return { allowed: false, reason: `Category ${category} not allowed for ${role}` }; } return { allowed: true }; }
Step 3: Domain Restrictions per Team
// Enforce domain restrictions so compliance-sensitive teams // only see results from vetted sources async function enforcedSearch( exa: Exa, role: string, query: string, opts: any = {} ) { const perms = ROLE_PERMISSIONS[role]; if (!perms) throw new Error(`Unknown role: ${role}`); const validation = validateSearchRequest( role, opts.type || "auto", opts.numResults || 10, opts.category ); if (!validation.allowed) throw new Error(validation.reason); return exa.searchAndContents(query, { ...opts, numResults: Math.min(opts.numResults || 10, perms.maxResults), type: opts.type || "auto", // Merge domain restrictions from role permissions includeDomains: perms.includeDomains || opts.includeDomains, }); }
Step 4: Per-Key Usage Tracking
// Track usage per API key / role for budget enforcement class KeyUsageTracker { private usage = new Map<string, { count: number; resetAt: number }>(); checkAndIncrement(role: string): void { const perms = ROLE_PERMISSIONS[role]; if (!perms) throw new Error(`Unknown role: ${role}`); const now = Date.now(); const dayStart = new Date().setHours(0, 0, 0, 0); let entry = this.usage.get(role); if (!entry || entry.resetAt < now) { entry = { count: 0, resetAt: dayStart + 24 * 60 * 60 * 1000 }; } if (entry.count >= perms.dailySearchLimit) { throw new Error( `Daily search limit (${perms.dailySearchLimit}) exceeded for ${role}` ); } entry.count++; this.usage.set(role, entry); } getUsage(role: string) { const entry = this.usage.get(role); const limit = ROLE_PERMISSIONS[role]?.dailySearchLimit || 0; return { used: entry?.count || 0, limit, remaining: limit - (entry?.count || 0), }; } }
Step 5: Key Rotation Procedure
set -euo pipefail # 1. Create new key in Exa dashboard (dashboard.exa.ai) # 2. Deploy new key alongside old key # 3. Verify new key works curl -s -o /dev/null -w "%{http_code}" \ -X POST https://api.exa.ai/search \ -H "x-api-key: $NEW_EXA_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"key rotation test","numResults":1}' # 4. Switch traffic to new key # 5. Monitor for errors # 6. Revoke old key in dashboard after 24h
Error Handling
| Issue | Cause | Solution |
|---|---|---|
on search | Invalid or revoked API key | Regenerate in dashboard |
| Key-level rate limit exceeded | Distribute across keys |
| Daily limit hit | Search budget exhausted | Adjust limits or wait for reset |
| Wrong domain results | Missing domain filter | Apply per role |
Resources
Next Steps
For policy enforcement, see
exa-policy-guardrails. For multi-env setup, see exa-multi-env-setup.