@agilehead/permiso-client
v0.0.6
Published
Client library for Permiso RBAC API
Downloads
47
Maintainers
Readme
@codespin/permiso-client
A TypeScript client library for the Permiso RBAC (Role-Based Access Control) API. This package provides a simple, type-safe way to interact with Permiso without needing to write GraphQL queries.
Features
- 🔒 Type-safe - Full TypeScript support with comprehensive type definitions
- 🚀 Zero GraphQL knowledge required - Simple function calls instead of query strings
- ⚡ Lightweight - Minimal dependencies, tree-shakeable
- 🛡️ Result types - Explicit error handling with discriminated unions
- 🔄 Consistent API - Uniform patterns across all operations
- 📦 Pagination support - Built-in pagination for list operations
- 🔍 Property filtering - Filter entities by custom properties
- 🎯 IDE friendly - Full auto-completion and inline documentation
Installation
npm install @codespin/permiso-clientQuick Start
import {
createTenant,
createUser,
assignUserRole,
hasPermission,
PermisoConfig,
} from "@codespin/permiso-client";
// Configure the client
const config: PermisoConfig = {
endpoint: "http://localhost:5001",
apiKey: "your-bearer-token", // optional - will be sent as Bearer token
timeout: 30000, // optional, in milliseconds
};
// Create a tenant
const tenantResult = await createTenant(config, {
id: "acme-corp",
name: "ACME Corporation",
description: "A sample tenant",
});
if (tenantResult.success) {
console.log("Created tenant:", tenantResult.data);
}
// Check if a user has permission
const hasPermResult = await hasPermission(config, {
tenantId: "acme-corp",
userId: "john-doe",
resourceId: "/api/users/*",
action: "read",
});
if (hasPermResult.success) {
console.log("Has permission:", hasPermResult.data);
}Configuration
Basic Configuration
import { PermisoConfig } from "@codespin/permiso-client";
const config: PermisoConfig = {
endpoint: "http://localhost:5001", // GraphQL endpoint URL
apiKey: "your-bearer-token", // Optional: Bearer token for authentication
timeout: 30000, // Optional: Request timeout in ms (default: 30000)
headers: {
// Optional: Additional headers
"X-Custom-Header": "value",
},
};Environment-based Configuration
const config: PermisoConfig = {
endpoint: process.env.PERMISO_ENDPOINT || "http://localhost:5001",
apiKey: process.env.PERMISO_API_KEY,
timeout: parseInt(process.env.PERMISO_TIMEOUT || "30000"),
};Error Handling
All functions return a Result type that explicitly handles success and failure cases:
const result = await createUser(config, {
id: "john-doe",
tenantId: "acme-corp",
identityProvider: "google",
identityProviderUserId: "[email protected]",
});
if (result.success) {
// Type-safe access to data
console.log("User created:", result.data.id);
} else {
// Type-safe access to error
console.error("Failed to create user:", result.error.message);
}API Reference
Tenants
getTenant(config, id)- Get a tenant by IDlistTenants(config, options?)- List tenants with optional filtering and paginationgetTenantsByIds(config, ids)- Get multiple tenants by IDscreateTenant(config, input)- Create a new tenantupdateTenant(config, id, input)- Update a tenantdeleteTenant(config, id, safetyKey?)- Delete a tenantgetTenantProperty(config, tenantId, propertyName)- Get a specific propertysetTenantProperty(config, tenantId, name, value, hidden?)- Set a propertydeleteTenantProperty(config, tenantId, name)- Delete a property
Users
getUser(config, tenantId, userId)- Get a userlistUsers(config, tenantId, options?)- List users with optional filtering and paginationgetUsersByIds(config, tenantId, ids)- Get multiple users by IDsgetUsersByIdentity(config, identityProvider, identityProviderUserId)- Find users by identitycreateUser(config, input)- Create a new userupdateUser(config, tenantId, userId, input)- Update a userdeleteUser(config, tenantId, userId)- Delete a usergetUserProperty(config, tenantId, userId, propertyName)- Get a user propertysetUserProperty(config, tenantId, userId, name, value, hidden?)- Set a user propertydeleteUserProperty(config, tenantId, userId, name)- Delete a user propertyassignUserRole(config, tenantId, userId, roleId)- Assign a role to a userunassignUserRole(config, tenantId, userId, roleId)- Remove a role from a user
Roles
getRole(config, tenantId, roleId)- Get a rolelistRoles(config, tenantId, options?)- List roles with optional filtering and paginationgetRolesByIds(config, tenantId, ids)- Get multiple roles by IDscreateRole(config, input)- Create a new roleupdateRole(config, tenantId, roleId, input)- Update a roledeleteRole(config, tenantId, roleId)- Delete a rolegetRoleProperty(config, tenantId, roleId, propertyName)- Get a role propertysetRoleProperty(config, tenantId, roleId, name, value, hidden?)- Set a role propertydeleteRoleProperty(config, tenantId, roleId, name)- Delete a role property
Resources
getResource(config, tenantId, resourceId)- Get a resourcelistResources(config, tenantId, options?)- List resources with optional filtering and paginationgetResourcesByIdPrefix(config, tenantId, idPrefix)- Get resources by ID prefixcreateResource(config, input)- Create a new resourceupdateResource(config, tenantId, resourceId, input)- Update a resourcedeleteResource(config, tenantId, resourceId)- Delete a resource
Permissions
hasPermission(config, params)- Check if a user has permissiongetUserPermissions(config, params)- Get user permissionsgetRolePermissions(config, params)- Get role permissionsgetEffectivePermissions(config, params)- Get effective permissions for a usergetEffectivePermissionsByPrefix(config, params)- Get effective permissions by resource prefixgrantUserPermission(config, input)- Grant permission to a userrevokeUserPermission(config, params)- Revoke permission from a usergrantRolePermission(config, input)- Grant permission to a rolerevokeRolePermission(config, params)- Revoke permission from a role
Pagination
List operations support pagination through the PaginationInput type:
const result = await listUsers(config, "acme-corp", {
pagination: {
limit: 10,
offset: 20,
sortDirection: "DESC", // "ASC" or "DESC", defaults to "ASC"
},
});
if (result.success) {
console.log("Users:", result.data.nodes);
console.log("Total count:", result.data.totalCount);
console.log("Has next page:", result.data.pageInfo.hasNextPage);
}Filtering
List operations support filtering by properties:
const result = await listUsers(config, "acme-corp", {
filter: {
properties: [
{ name: "department", value: "engineering" },
{ name: "active", value: true },
],
},
});// Set a hidden property (e.g., for sensitive data)
await setUserProperty(
config,
"acme-corp",
"john-doe",
"apiToken",
"secret-token-123",
true, // hidden = true
);Property Filtering
Filter entities by their properties:
// Set a property
const setPropResult = await setUserProperty(
config,
"acme-corp",
"john-doe",
"preferences",
{ theme: "dark", language: "en" },
false, // not hidden
);
// Get a property
const getPropResult = await getUserProperty(
config,
"acme-corp",
"john-doe",
"preferences",
);
if (getPropResult.success && getPropResult.data) {
console.log("User preferences:", getPropResult.data.value);
}Advanced Usage
Batch Operations
For better performance when creating multiple entities:
// Create multiple users efficiently
const users = [
{
id: "user-1",
tenantId: "tenant-1",
identityProvider: "auth0",
identityProviderUserId: "auth0|123",
},
{
id: "user-2",
tenantId: "tenant-1",
identityProvider: "auth0",
identityProviderUserId: "auth0|456",
},
{
id: "user-3",
tenantId: "tenant-1",
identityProvider: "auth0",
identityProviderUserId: "auth0|789",
},
];
const results = await Promise.all(
users.map((user) => createUser(config, user)),
);
const failed = results.filter((r) => !r.success);
if (failed.length > 0) {
console.error("Some users failed to create:", failed);
}Permission Checking Patterns
// Check single permission
const canRead = await hasPermission(config, {
tenantId: "acme-corp",
userId: "john-doe",
resourceId: "/api/users/*",
action: "read",
});
// Check multiple permissions
const permissions = await Promise.all([
hasPermission(config, {
tenantId,
userId,
resourceId: "/api/users/*",
action: "read",
}),
hasPermission(config, {
tenantId,
userId,
resourceId: "/api/users/*",
action: "write",
}),
hasPermission(config, {
tenantId,
userId,
resourceId: "/api/billing/*",
action: "read",
}),
]);
const [canReadUsers, canWriteUsers, canReadBilling] = permissions.map(
(r) => r.success && r.data,
);
// Get all effective permissions for a user
const effectivePerms = await getEffectivePermissions(config, {
tenantId: "acme-corp",
userId: "john-doe",
});
if (effectivePerms.success) {
const groupedByResource = effectivePerms.data.reduce(
(acc, perm) => {
if (!acc[perm.resourceId]) acc[perm.resourceId] = [];
acc[perm.resourceId].push(perm.action);
return acc;
},
{} as Record<string, string[]>,
);
console.log("Permissions by resource:", groupedByResource);
}Resource Path Patterns
Permiso uses Unix-like path patterns for resources:
// Exact match
const resource1 = await createResource(config, {
id: "/api/users",
tenantId: "acme-corp",
description: "User management API",
});
// Wildcard match (matches any sub-path)
const resource2 = await createResource(config, {
id: "/api/users/*",
tenantId: "acme-corp",
description: "All user endpoints",
});
// Specific endpoint
const resource3 = await createResource(config, {
id: "/api/users/profile",
tenantId: "acme-corp",
description: "User profile endpoint",
});
// Hierarchical resources
const resource4 = await createResource(config, {
id: "/api/billing/invoices/*",
tenantId: "acme-corp",
description: "Invoice management",
});Handling Pagination
// Fetch all users page by page
async function getAllUsers(config: PermisoConfig, tenantId: string) {
const allUsers = [];
let offset = 0;
const limit = 50;
while (true) {
const result = await listUsers(config, tenantId, {
pagination: { limit, offset },
});
if (!result.success) {
throw new Error(`Failed to fetch users: ${result.error.message}`);
}
allUsers.push(...result.data.nodes);
if (!result.data.pageInfo.hasNextPage) {
break;
}
offset += limit;
}
return allUsers;
}Types
The client exports all TypeScript types from the Permiso API:
import type {
// Core entities
Tenant,
User,
Role,
Resource,
Permission,
Property,
// Input types
CreateTenantInput,
CreateUserInput,
CreateRoleInput,
CreateResourceInput,
UpdateTenantInput,
UpdateUserInput,
UpdateRoleInput,
// Permission types
UserPermission,
RolePermission,
EffectivePermission,
// Utility types
PaginationInput,
Connection,
PageInfo,
Result,
// Configuration
PermisoConfig,
} from "@codespin/permiso-client";Best Practices
1. Configuration Management
Store configuration in a central location:
// config/permiso.ts
export const permisoConfig: PermisoConfig = {
endpoint: process.env.PERMISO_ENDPOINT!,
apiKey: process.env.PERMISO_API_KEY,
};
// Usage in other files
import { permisoConfig } from "./config/permiso";
import { createUser } from "@codespin/permiso-client";
const result = await createUser(permisoConfig, userData);2. Error Handling Wrapper
Create a wrapper for consistent error handling:
async function executePermiso<T>(
operation: Promise<Result<T>>,
errorMessage: string,
): Promise<T> {
const result = await operation;
if (!result.success) {
console.error(`${errorMessage}:`, result.error);
throw new Error(`${errorMessage}: ${result.error.message}`);
}
return result.data;
}
// Usage
const user = await executePermiso(
createUser(config, userData),
"Failed to create user",
);3. Type Guards
Use type guards for property values:
interface UserPreferences {
theme: "light" | "dark";
language: string;
notifications: boolean;
}
function isUserPreferences(value: unknown): value is UserPreferences {
return (
typeof value === "object" &&
value !== null &&
"theme" in value &&
"language" in value &&
"notifications" in value
);
}
// Usage
const prefResult = await getUserProperty(config, tenantId, userId, "preferences");
if (prefResult.success && prefResult.data) {
const value = prefResult.data.value;
if (isUserPreferences(value)) {
console.log("User theme:", value.theme);
}
}4. Resource Naming Conventions
Follow consistent patterns for resource IDs:
// API endpoints
"/api/users";
"/api/users/*";
"/api/users/{id}";
"/api/users/{id}/profile";
// Feature-based
"/features/billing";
"/features/billing/*";
"/features/reporting";
// Service-based
"/services/auth";
"/services/notifications";
"/services/analytics/*";Troubleshooting
Connection Issues
// Add retry logic for transient failures
async function withRetry<T>(
operation: () => Promise<Result<T>>,
maxRetries = 3,
): Promise<Result<T>> {
for (let i = 0; i < maxRetries; i++) {
const result = await operation();
if (result.success || i === maxRetries - 1) {
return result;
}
// Check if error is retryable
if (
result.error.message.includes("ECONNREFUSED") ||
result.error.message.includes("ETIMEDOUT")
) {
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
continue;
}
return result;
}
return { success: false, error: new Error("Max retries exceeded") };
}Debugging
Enable debug logging:
// Create a debugging wrapper
function createDebugClient(config: PermisoConfig) {
return new Proxy(
{},
{
get(target, prop) {
const original = (await import("@codespin/permiso-client"))[prop];
if (typeof original === "function") {
return async (...args) => {
console.log(`Calling ${String(prop)} with:`, args);
const result = await original(...args);
console.log(`Result:`, result);
return result;
};
}
return original;
},
},
);
}Migration Guide
From Direct GraphQL
If migrating from direct GraphQL queries:
// Before (GraphQL)
const query = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
tenantId
}
}
`;
const result = await graphqlClient.request(query, { input: userData });
// After (Client)
const result = await createUser(config, userData);
if (result.success) {
console.log(result.data.id, result.data.tenantId);
}Testing
The client package includes comprehensive integration tests that verify all API operations against a real Permiso server.
Running Tests
# From the project root
npm run test:client
# Run specific test suite
npm run test:client:grep -- "Tenants"Test Coverage
The test suite covers all API operations:
- Tenants (12 tests): CRUD, properties, pagination
- Users (11 tests): CRUD, role assignment, properties, search
- Roles (13 tests): CRUD, hidden properties, filtering
- Resources (13 tests): CRUD, wildcards, hierarchical paths
- Permissions (15 tests): Grants, effective permissions, inheritance
Test Infrastructure
- Uses separate test database (
permiso_client_test) - Runs on port 5003 (isolated from main server)
- Database cleaned between tests
- Migrations run automatically
API Stability
This client follows semantic versioning. The API is stable and breaking changes will only be introduced in major versions.
Contributing
Contributions are welcome! Please see the main Permiso repository for contribution guidelines.
License
MIT © Codespin
