From 95db636b290a8632de4923d9bd53a65a04e9a265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Goran=20Jovanovi=C4=87?= Date: Sat, 29 Nov 2025 20:16:47 +0100 Subject: [PATCH] feat(permissio): add main SDK client and permission checks - Implement main Permissio SDK client with methods for checking permissions, managing users, and assigning roles. - Introduce types for permission checks, user roles, and resources. - Add bulk permission checking and user synchronization functionalities. - Ensure scope initialization for API key management. - Include debug logging for better traceability during permission evaluations. --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 4 +- LICENSE | 2 +- README.md | 1080 +++++++++--------- examples/browser-demo/README.md | 6 +- examples/browser-demo/index.html | 6 +- examples/browser-demo/package-lock.json | 10 +- examples/browser-demo/package.json | 6 +- examples/browser-demo/src/main.ts | 1158 ++++++++++---------- examples/browser-demo/vite.config.ts | 22 +- package-lock.json | 4 +- package.json | 8 +- src/api/base.ts | 598 +++++----- src/api/index.ts | 14 +- src/api/resources.ts | 380 +++---- src/api/role-assignments.ts | 474 ++++---- src/api/roles.ts | 294 ++--- src/api/tenants.ts | 222 ++-- src/api/users.ts | 252 ++--- src/config/index.ts | 310 +++--- src/index.ts | 224 ++-- src/{permis.ts => permissio.ts} | 1332 +++++++++++------------ src/types/check.ts | 220 ++-- src/types/index.ts | 194 ++-- src/types/resource.ts | 178 +-- src/types/role-assignment.ts | 152 +-- src/types/role.ts | 120 +- src/types/tenant.ts | 104 +- src/types/user.ts | 112 +- tsup.config.ts | 24 +- 30 files changed, 3756 insertions(+), 3756 deletions(-) rename src/{permis.ts => permissio.ts} (90%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42f930c..035d9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to the Permis.io Node.js SDK will be documented in this file. +All notable changes to the Permissio.io Node.js SDK will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f9d2d9..5f7ac99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to Permis.io Node.js SDK +# Contributing to Permissio.io Node.js SDK -Thank you for your interest in contributing to the Permis.io Node.js SDK! This document provides guidelines and steps for contributing. +Thank you for your interest in contributing to the Permissio.io Node.js SDK! This document provides guidelines and steps for contributing. ## Code of Conduct diff --git a/LICENSE b/LICENSE index 14610a3..e57a74f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Permis.io Contributors +Copyright (c) 2025 Permissio.io Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 374b0a7..e56e58f 100644 --- a/README.md +++ b/README.md @@ -1,540 +1,540 @@ -# Permis.io Node.js SDK - -Official Node.js SDK for [Permis.io](https://permis.io) - Authorization as a Service. - -## Installation - -```bash -npm install permisio -# or -yarn add permisio -# or -pnpm add permisio -``` - -## Quick Start - -```typescript -import { Permis } from 'permisio'; - -// Initialize the SDK -const permis = new Permis({ - token: 'permis_key_your_api_key_here', - projectId: 'your-project-id', - environmentId: 'your-environment-id', -}); - -// Check permissions -const allowed = await permis.check({ - user: 'user@example.com', - action: 'read', - resource: 'document', -}); - -if (allowed) { - console.log('Access granted!'); -} else { - console.log('Access denied!'); -} -``` - -## Configuration - -```typescript -import { Permis } from 'permisio'; - -const permis = new Permis({ - // Required: Your API key - token: 'permis_key_your_api_key_here', - - // Optional: API base URL (defaults to https://api.permis.io) - apiUrl: 'https://api.permis.io', - - // Optional: Project and Environment IDs - projectId: 'your-project-id', - environmentId: 'your-environment-id', - - // Optional: Enable debug logging - debug: false, - - // Optional: Request timeout in ms (default: 30000) - timeout: 30000, - - // Optional: Number of retry attempts (default: 3) - retryAttempts: 3, - - // Optional: Throw errors or return false (default: true) - throwOnError: true, - - // Optional: Custom headers - customHeaders: { - 'X-Custom-Header': 'value', - }, -}); -``` - -## Permission Checks - -### Basic Check - -```typescript -const allowed = await permis.check({ - user: 'user@example.com', - action: 'read', - resource: 'document', -}); -``` - -### Check with Resource Instance - -```typescript -const allowed = await permis.check({ - user: 'user@example.com', - action: 'edit', - resource: { - type: 'document', - key: 'doc-123', - }, -}); -``` - -### Check with Tenant Context - -```typescript -const allowed = await permis.check({ - user: 'user@example.com', - action: 'delete', - resource: 'document', - tenant: 'acme-corp', -}); -``` - -### Check with User Attributes - -```typescript -const allowed = await permis.check({ - user: { - key: 'user@example.com', - attributes: { - department: 'engineering', - level: 'senior', - }, - }, - action: 'approve', - resource: 'expense_report', -}); -``` - -### Get Full Check Response - -```typescript -const response = await permis.checkWithDetails({ - user: 'user@example.com', - action: 'read', - resource: 'document', -}); - -console.log(response.allowed); // boolean -console.log(response.reason); // string (optional) -console.log(response.debug); // debug info (when debug enabled) -``` - -### Bulk Permission Checks - -```typescript -const results = await permis.bulkCheck({ - checks: [ - { user: 'user1@example.com', action: 'read', resource: 'document' }, - { user: 'user1@example.com', action: 'write', resource: 'document' }, - { user: 'user2@example.com', action: 'read', resource: 'document' }, - ], -}); - -results.results.forEach(({ request, response }) => { - console.log(`${request.user} ${request.action} ${request.resource}: ${response.allowed}`); -}); -``` - -### Check and Throw - -```typescript -try { - await permis.checkAndThrow({ - user: 'user@example.com', - action: 'delete', - resource: 'document', - }); - // Access granted, continue with operation -} catch (error) { - // Access denied - console.error(error.message); -} -``` - -## User Management - -### Create a User - -```typescript -const user = await permis.api.users.create({ - key: 'user@example.com', - email: 'user@example.com', - firstName: 'John', - lastName: 'Doe', - attributes: { - department: 'engineering', - }, -}); -``` - -### Sync User (Create or Update) - -```typescript -const user = await permis.api.users.sync({ - key: 'user@example.com', - email: 'user@example.com', - firstName: 'John', - lastName: 'Doe', -}); -``` - -### Sync User with Roles - -```typescript -await permis.syncUser({ - key: 'user@example.com', - email: 'user@example.com', - firstName: 'John', - lastName: 'Doe', - roles: [ - { role: 'admin', tenant: 'acme-corp' }, - { role: 'viewer' }, - ], -}); -``` - -### List Users - -```typescript -const users = await permis.api.users.list({ - page: 1, - perPage: 10, - search: 'john', -}); -``` - -### Get User Roles - -```typescript -const roles = await permis.api.users.getRoles('user@example.com'); -``` - -### Assign Role to User - -```typescript -await permis.api.users.assignRole('user@example.com', 'admin', 'acme-corp'); -``` - -## Tenant Management - -### Create a Tenant - -```typescript -const tenant = await permis.api.tenants.create({ - key: 'acme-corp', - name: 'Acme Corporation', - description: 'Main organization', - attributes: { - plan: 'enterprise', - }, -}); -``` - -### Get Tenant Users - -```typescript -const users = await permis.api.tenants.getUsers('acme-corp'); -``` - -### Add User to Tenant - -```typescript -await permis.api.tenants.addUser('acme-corp', 'user@example.com'); -``` - -## Role Management - -### Create a Role - -```typescript -const role = await permis.api.roles.create({ - key: 'editor', - name: 'Editor', - description: 'Can edit documents', - permissions: ['document:read', 'document:write'], -}); -``` - -### Add Permission to Role - -```typescript -await permis.api.roles.addPermission('editor', 'document:delete'); -``` - -### Role Inheritance - -```typescript -// Create a role that extends another -await permis.api.roles.create({ - key: 'admin', - name: 'Admin', - extends: ['editor'], - permissions: ['document:delete', 'user:manage'], -}); -``` - -## Resource Management - -### Create a Resource Type - -```typescript -const resource = await permis.api.resources.create({ - key: 'document', - name: 'Document', - actions: ['read', 'write', 'delete', 'share'], -}); -``` - -### Create a Resource Instance - -```typescript -const instance = await permis.api.resources.createInstance({ - key: 'doc-123', - resourceType: 'document', - tenant: 'acme-corp', - attributes: { - title: 'My Document', - owner: 'user@example.com', - }, -}); -``` - -## Role Assignments - -### Assign a Role - -```typescript -await permis.api.roleAssignments.assign({ - user: 'user@example.com', - role: 'admin', - tenant: 'acme-corp', -}); -``` - -### Assign Role on Resource - -```typescript -await permis.api.roleAssignments.assign({ - user: 'user@example.com', - role: 'editor', - resource: 'document', - resourceInstance: 'doc-123', -}); -``` - -### Bulk Role Assignment - -```typescript -const result = await permis.api.roleAssignments.bulkAssign([ - { user: 'user1@example.com', role: 'viewer', tenant: 'acme-corp' }, - { user: 'user2@example.com', role: 'editor', tenant: 'acme-corp' }, - { user: 'user3@example.com', role: 'admin', tenant: 'acme-corp' }, -]); - -console.log(`Created: ${result.created}, Failed: ${result.failed}`); -``` - -### Check if User Has Role - -```typescript -const hasRole = await permis.api.roleAssignments.hasRole( - 'user@example.com', - 'admin', - { tenant: 'acme-corp' } -); -``` - -## Get User Permissions - -```typescript -const permissions = await permis.getPermissions({ - user: 'user@example.com', - tenant: 'acme-corp', -}); - -console.log(permissions.roles); // ['admin', 'editor'] -console.log(permissions.permissions); // ['document:read', 'document:write', ...] -``` - -## Error Handling - -```typescript -import { Permis, PermisApiError } from 'permisio'; - -const permis = new Permis({ - token: 'permis_key_...', - throwOnError: true, // default -}); - -try { - await permis.api.users.get('nonexistent-user'); -} catch (error) { - if (error instanceof PermisApiError) { - console.error('API Error:', error.message); - console.error('Status Code:', error.statusCode); - console.error('Error Code:', error.code); - console.error('Details:', error.details); - } -} -``` - -### Disable Throwing Errors - -```typescript -const permis = new Permis({ - token: 'permis_key_...', - throwOnError: false, -}); - -// Will return false instead of throwing -const allowed = await permis.check({ - user: 'user@example.com', - action: 'read', - resource: 'document', -}); -``` - -## TypeScript Support - -This SDK is written in TypeScript and provides full type definitions. - -```typescript -import { - Permis, - IPermisConfig, - ICheckRequest, - ICheckResponse, - IUserCreate, - IUserRead, - IRoleAssignmentCreate, -} from 'permisio'; - -const config: IPermisConfig = { - token: 'permis_key_...', - projectId: 'my-project', - environmentId: 'production', -}; - -const permis = new Permis(config); - -const request: ICheckRequest = { - user: 'user@example.com', - action: 'read', - resource: 'document', -}; - -const response: ICheckResponse = await permis.checkWithDetails(request); -``` - -## Framework Integration Examples - -### Express.js Middleware - -```typescript -import { Permis } from 'permisio'; -import express from 'express'; - -const permis = new Permis({ - token: process.env.PERMIS_API_KEY!, - projectId: process.env.PERMIS_PROJECT_ID!, - environmentId: process.env.PERMIS_ENVIRONMENT_ID!, -}); - -// Middleware factory -function requirePermission(action: string, getResource: (req: express.Request) => string) { - return async (req: express.Request, res: express.Response, next: express.NextFunction) => { - const userId = req.user?.id; // Assuming user is attached to request - - if (!userId) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const allowed = await permis.check({ - user: userId, - action, - resource: getResource(req), - }); - - if (!allowed) { - return res.status(403).json({ error: 'Forbidden' }); - } - - next(); - }; -} - -// Usage -app.get( - '/documents/:id', - requirePermission('read', (req) => ({ type: 'document', key: req.params.id })), - async (req, res) => { - // Handle request - } -); -``` - -### NestJS Guard - -```typescript -import { Injectable, CanActivate, ExecutionContext, SetMetadata } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { Permis } from 'permisio'; - -export const PERMISSION_KEY = 'permission'; -export const RequirePermission = (action: string, resource: string) => - SetMetadata(PERMISSION_KEY, { action, resource }); - -@Injectable() -export class PermisGuard implements CanActivate { - constructor( - private reflector: Reflector, - private permis: Permis - ) {} - - async canActivate(context: ExecutionContext): Promise { - const permission = this.reflector.get(PERMISSION_KEY, context.getHandler()); - if (!permission) return true; - - const request = context.switchToHttp().getRequest(); - const userId = request.user?.id; - - if (!userId) return false; - - return this.permis.check({ - user: userId, - action: permission.action, - resource: permission.resource, - }); - } -} -``` - -## License - -MIT - -## Support - -- Documentation: [https://docs.permis.io](https://docs.permis.io) -- Issues: [GitHub Issues](https://github.com/permisio/permisio-node/issues) -- Email: support@permis.io +# Permissio.io Node.js SDK + +Official Node.js SDK for [Permissio.io](https://permissio.io) - Authorization as a Service. + +## Installation + +```bash +npm install permissio +# or +yarn add permissio +# or +pnpm add permissio +``` + +## Quick Start + +```typescript +import { Permissio } from 'permissio'; + +// Initialize the SDK +const permissio = new Permissio({ + token: 'permis_key_your_api_key_here', + projectId: 'your-project-id', + environmentId: 'your-environment-id', +}); + +// Check permissions +const allowed = await permissio.check({ + user: 'user@example.com', + action: 'read', + resource: 'document', +}); + +if (allowed) { + console.log('Access granted!'); +} else { + console.log('Access denied!'); +} +``` + +## Configuration + +```typescript +import { Permissio } from 'permissio'; + +const permissio = new Permissio({ + // Required: Your API key + token: 'permis_key_your_api_key_here', + + // Optional: API base URL (defaults to https://api.permissio.io) + apiUrl: 'https://api.permissio.io', + + // Optional: Project and Environment IDs + projectId: 'your-project-id', + environmentId: 'your-environment-id', + + // Optional: Enable debug logging + debug: false, + + // Optional: Request timeout in ms (default: 30000) + timeout: 30000, + + // Optional: Number of retry attempts (default: 3) + retryAttempts: 3, + + // Optional: Throw errors or return false (default: true) + throwOnError: true, + + // Optional: Custom headers + customHeaders: { + 'X-Custom-Header': 'value', + }, +}); +``` + +## Permission Checks + +### Basic Check + +```typescript +const allowed = await permissio.check({ + user: 'user@example.com', + action: 'read', + resource: 'document', +}); +``` + +### Check with Resource Instance + +```typescript +const allowed = await permissio.check({ + user: 'user@example.com', + action: 'edit', + resource: { + type: 'document', + key: 'doc-123', + }, +}); +``` + +### Check with Tenant Context + +```typescript +const allowed = await permissio.check({ + user: 'user@example.com', + action: 'delete', + resource: 'document', + tenant: 'acme-corp', +}); +``` + +### Check with User Attributes + +```typescript +const allowed = await permissio.check({ + user: { + key: 'user@example.com', + attributes: { + department: 'engineering', + level: 'senior', + }, + }, + action: 'approve', + resource: 'expense_report', +}); +``` + +### Get Full Check Response + +```typescript +const response = await permissio.checkWithDetails({ + user: 'user@example.com', + action: 'read', + resource: 'document', +}); + +console.log(response.allowed); // boolean +console.log(response.reason); // string (optional) +console.log(response.debug); // debug info (when debug enabled) +``` + +### Bulk Permission Checks + +```typescript +const results = await permissio.bulkCheck({ + checks: [ + { user: 'user1@example.com', action: 'read', resource: 'document' }, + { user: 'user1@example.com', action: 'write', resource: 'document' }, + { user: 'user2@example.com', action: 'read', resource: 'document' }, + ], +}); + +results.results.forEach(({ request, response }) => { + console.log(`${request.user} ${request.action} ${request.resource}: ${response.allowed}`); +}); +``` + +### Check and Throw + +```typescript +try { + await permissio.checkAndThrow({ + user: 'user@example.com', + action: 'delete', + resource: 'document', + }); + // Access granted, continue with operation +} catch (error) { + // Access denied + console.error(error.message); +} +``` + +## User Management + +### Create a User + +```typescript +const user = await permissio.api.users.create({ + key: 'user@example.com', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + attributes: { + department: 'engineering', + }, +}); +``` + +### Sync User (Create or Update) + +```typescript +const user = await permissio.api.users.sync({ + key: 'user@example.com', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', +}); +``` + +### Sync User with Roles + +```typescript +await permissio.syncUser({ + key: 'user@example.com', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + roles: [ + { role: 'admin', tenant: 'acme-corp' }, + { role: 'viewer' }, + ], +}); +``` + +### List Users + +```typescript +const users = await permissio.api.users.list({ + page: 1, + perPage: 10, + search: 'john', +}); +``` + +### Get User Roles + +```typescript +const roles = await permissio.api.users.getRoles('user@example.com'); +``` + +### Assign Role to User + +```typescript +await permissio.api.users.assignRole('user@example.com', 'admin', 'acme-corp'); +``` + +## Tenant Management + +### Create a Tenant + +```typescript +const tenant = await permissio.api.tenants.create({ + key: 'acme-corp', + name: 'Acme Corporation', + description: 'Main organization', + attributes: { + plan: 'enterprise', + }, +}); +``` + +### Get Tenant Users + +```typescript +const users = await permissio.api.tenants.getUsers('acme-corp'); +``` + +### Add User to Tenant + +```typescript +await permissio.api.tenants.addUser('acme-corp', 'user@example.com'); +``` + +## Role Management + +### Create a Role + +```typescript +const role = await permissio.api.roles.create({ + key: 'editor', + name: 'Editor', + description: 'Can edit documents', + permissions: ['document:read', 'document:write'], +}); +``` + +### Add Permission to Role + +```typescript +await permissio.api.roles.addPermission('editor', 'document:delete'); +``` + +### Role Inheritance + +```typescript +// Create a role that extends another +await permissio.api.roles.create({ + key: 'admin', + name: 'Admin', + extends: ['editor'], + permissions: ['document:delete', 'user:manage'], +}); +``` + +## Resource Management + +### Create a Resource Type + +```typescript +const resource = await permissio.api.resources.create({ + key: 'document', + name: 'Document', + actions: ['read', 'write', 'delete', 'share'], +}); +``` + +### Create a Resource Instance + +```typescript +const instance = await permissio.api.resources.createInstance({ + key: 'doc-123', + resourceType: 'document', + tenant: 'acme-corp', + attributes: { + title: 'My Document', + owner: 'user@example.com', + }, +}); +``` + +## Role Assignments + +### Assign a Role + +```typescript +await permissio.api.roleAssignments.assign({ + user: 'user@example.com', + role: 'admin', + tenant: 'acme-corp', +}); +``` + +### Assign Role on Resource + +```typescript +await permissio.api.roleAssignments.assign({ + user: 'user@example.com', + role: 'editor', + resource: 'document', + resourceInstance: 'doc-123', +}); +``` + +### Bulk Role Assignment + +```typescript +const result = await permissio.api.roleAssignments.bulkAssign([ + { user: 'user1@example.com', role: 'viewer', tenant: 'acme-corp' }, + { user: 'user2@example.com', role: 'editor', tenant: 'acme-corp' }, + { user: 'user3@example.com', role: 'admin', tenant: 'acme-corp' }, +]); + +console.log(`Created: ${result.created}, Failed: ${result.failed}`); +``` + +### Check if User Has Role + +```typescript +const hasRole = await permissio.api.roleAssignments.hasRole( + 'user@example.com', + 'admin', + { tenant: 'acme-corp' } +); +``` + +## Get User Permissions + +```typescript +const permissions = await permissio.getPermissions({ + user: 'user@example.com', + tenant: 'acme-corp', +}); + +console.log(permissions.roles); // ['admin', 'editor'] +console.log(permissions.permissions); // ['document:read', 'document:write', ...] +``` + +## Error Handling + +```typescript +import { Permissio, PermissioApiError } from 'permissio'; + +const permissio = new Permissio({ + token: 'permis_key_...', + throwOnError: true, // default +}); + +try { + await permissio.api.users.get('nonexistent-user'); +} catch (error) { + if (error instanceof PermissioApiError) { + console.error('API Error:', error.message); + console.error('Status Code:', error.statusCode); + console.error('Error Code:', error.code); + console.error('Details:', error.details); + } +} +``` + +### Disable Throwing Errors + +```typescript +const permissio = new Permissio({ + token: 'permis_key_...', + throwOnError: false, +}); + +// Will return false instead of throwing +const allowed = await permissio.check({ + user: 'user@example.com', + action: 'read', + resource: 'document', +}); +``` + +## TypeScript Support + +This SDK is written in TypeScript and provides full type definitions. + +```typescript +import { + Permis, + IPermisConfig, + ICheckRequest, + ICheckResponse, + IUserCreate, + IUserRead, + IRoleAssignmentCreate, +} from 'permissio'; + +const config: IPermisConfig = { + token: 'permis_key_...', + projectId: 'my-project', + environmentId: 'production', +}; + +const permissio = new Permissio(config); + +const request: ICheckRequest = { + user: 'user@example.com', + action: 'read', + resource: 'document', +}; + +const response: ICheckResponse = await permissio.checkWithDetails(request); +``` + +## Framework Integration Examples + +### Express.js Middleware + +```typescript +import { Permissio } from 'permissio'; +import express from 'express'; + +const permissio = new Permissio({ + token: process.env.PERMIS_API_KEY!, + projectId: process.env.PERMIS_PROJECT_ID!, + environmentId: process.env.PERMIS_ENVIRONMENT_ID!, +}); + +// Middleware factory +function requirePermission(action: string, getResource: (req: express.Request) => string) { + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { + const userId = req.user?.id; // Assuming user is attached to request + + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const allowed = await permissio.check({ + user: userId, + action, + resource: getResource(req), + }); + + if (!allowed) { + return res.status(403).json({ error: 'Forbidden' }); + } + + next(); + }; +} + +// Usage +app.get( + '/documents/:id', + requirePermission('read', (req) => ({ type: 'document', key: req.params.id })), + async (req, res) => { + // Handle request + } +); +``` + +### NestJS Guard + +```typescript +import { Injectable, CanActivate, ExecutionContext, SetMetadata } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Permissio } from 'permissio'; + +export const PERMISSION_KEY = 'permission'; +export const RequirePermission = (action: string, resource: string) => + SetMetadata(PERMISSION_KEY, { action, resource }); + +@Injectable() +export class PermisGuard implements CanActivate { + constructor( + private reflector: Reflector, + private permis: Permis + ) {} + + async canActivate(context: ExecutionContext): Promise { + const permission = this.reflector.get(PERMISSION_KEY, context.getHandler()); + if (!permission) return true; + + const request = context.switchToHttp().getRequest(); + const userId = request.user?.id; + + if (!userId) return false; + + return this.permissio.check({ + user: userId, + action: permission.action, + resource: permission.resource, + }); + } +} +``` + +## License + +MIT + +## Support + +- Documentation: [https://docs.permissio.io](https://docs.permissio.io) +- Issues: [GitHub Issues](https://github.com/permissio/permissio-node/issues) +- Email: support@permissio.io diff --git a/examples/browser-demo/README.md b/examples/browser-demo/README.md index 71d1c36..d3dde98 100644 --- a/examples/browser-demo/README.md +++ b/examples/browser-demo/README.md @@ -1,6 +1,6 @@ -# Permis.io Browser Demo +# Permissio.io Browser Demo -Interactive browser-based demo application for testing and demonstrating the Permis.io Node.js SDK. +Interactive browser-based demo application for testing and demonstrating the Permissio.io Node.js SDK. ## Features @@ -13,7 +13,7 @@ Interactive browser-based demo application for testing and demonstrating the Per ## Prerequisites -1. Make sure the Permis.io backend is running (default: `http://localhost:3001`) +1. Make sure the Permissio.io backend is running (default: `http://localhost:3001`) 2. Have an API key ready (format: `permis_key_xxxxxxxx...`) 3. Know your Project ID and Environment ID diff --git a/examples/browser-demo/index.html b/examples/browser-demo/index.html index f2065a7..b7462fb 100644 --- a/examples/browser-demo/index.html +++ b/examples/browser-demo/index.html @@ -3,14 +3,14 @@ - Permis.io SDK Demo + Permissio.io SDK Demo
-

🔐 Permis.io SDK Demo

-

Interactive demo for testing the Permis.io Node.js SDK

+

🔐 Permissio.io SDK Demo

+

Interactive demo for testing the Permissio.io Node.js SDK

diff --git a/examples/browser-demo/package-lock.json b/examples/browser-demo/package-lock.json index ba4db76..a36eea3 100644 --- a/examples/browser-demo/package-lock.json +++ b/examples/browser-demo/package-lock.json @@ -1,14 +1,14 @@ { - "name": "permisio-browser-demo", + "name": "permissio-browser-demo", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "permisio-browser-demo", + "name": "permissio-browser-demo", "version": "1.0.0", "dependencies": { - "permisio": "file:../../" + "permissio": "file:../../" }, "devDependencies": { "typescript": "^5.6.0", @@ -16,7 +16,7 @@ } }, "../..": { - "name": "permisio", + "name": "permissio", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -817,7 +817,7 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/permisio": { + "node_modules/permissio": { "resolved": "../..", "link": true }, diff --git a/examples/browser-demo/package.json b/examples/browser-demo/package.json index 15c2544..5a357f3 100644 --- a/examples/browser-demo/package.json +++ b/examples/browser-demo/package.json @@ -1,7 +1,7 @@ { - "name": "permisio-browser-demo", + "name": "permissio-browser-demo", "version": "1.0.0", - "description": "Browser demo for Permis.io SDK", + "description": "Browser demo for Permissio.io SDK", "type": "module", "scripts": { "dev": "vite", @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "permisio": "file:../../" + "permissio": "file:../../" }, "devDependencies": { "vite": "^5.4.0", diff --git a/examples/browser-demo/src/main.ts b/examples/browser-demo/src/main.ts index e5c8525..1c1d995 100644 --- a/examples/browser-demo/src/main.ts +++ b/examples/browser-demo/src/main.ts @@ -1,579 +1,579 @@ -import { Permis } from "permisio"; - -// Global SDK instance -let permis: Permis | null = null; - -// Console logging helper -function log( - message: string, - type: "log" | "info" | "success" | "error" | "warn" = "log" -) { - const consoleEl = document.getElementById("console")!; - const time = new Date().toLocaleTimeString(); - const entry = document.createElement("div"); - entry.className = `console-entry ${type}`; - entry.innerHTML = `[${time}]${escapeHtml(message)}`; - consoleEl.appendChild(entry); - consoleEl.scrollTop = consoleEl.scrollHeight; -} - -function escapeHtml(text: string): string { - const div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; -} - -// Tab switching -function setupTabs() { - const tabs = document.querySelectorAll(".tab"); - tabs.forEach((tab) => { - tab.addEventListener("click", () => { - const tabName = (tab as HTMLElement).dataset.tab; - - // Update active tab - tabs.forEach((t) => t.classList.remove("active")); - tab.classList.add("active"); - - // Update active content - document.querySelectorAll(".tab-content").forEach((content) => { - content.classList.remove("active"); - }); - document.getElementById(`tab-${tabName}`)?.classList.add("active"); - }); - }); -} - -// Show/hide forms -function setupFormToggles() { - // Users - document.getElementById("showCreateUser")?.addEventListener("click", () => { - document.getElementById("createUserForm")?.classList.remove("hidden"); - }); - document.getElementById("cancelCreateUser")?.addEventListener("click", () => { - document.getElementById("createUserForm")?.classList.add("hidden"); - }); - - // Roles - document.getElementById("showCreateRole")?.addEventListener("click", () => { - document.getElementById("createRoleForm")?.classList.remove("hidden"); - }); - document.getElementById("cancelCreateRole")?.addEventListener("click", () => { - document.getElementById("createRoleForm")?.classList.add("hidden"); - }); - - // Tenants - document.getElementById("showCreateTenant")?.addEventListener("click", () => { - document.getElementById("createTenantForm")?.classList.remove("hidden"); - }); - document - .getElementById("cancelCreateTenant") - ?.addEventListener("click", () => { - document.getElementById("createTenantForm")?.classList.add("hidden"); - }); - - // Resources - document - .getElementById("showCreateResource") - ?.addEventListener("click", () => { - document.getElementById("createResourceForm")?.classList.remove("hidden"); - }); - document - .getElementById("cancelCreateResource") - ?.addEventListener("click", () => { - document.getElementById("createResourceForm")?.classList.add("hidden"); - }); - - // Role Assignments - document.getElementById("showAssignRole")?.addEventListener("click", () => { - document.getElementById("assignRoleForm")?.classList.remove("hidden"); - }); - document.getElementById("cancelAssignRole")?.addEventListener("click", () => { - document.getElementById("assignRoleForm")?.classList.add("hidden"); - }); -} - -// SDK Status helper -function setStatus(message: string, type: "success" | "error" | "info") { - const statusEl = document.getElementById("sdkStatus")!; - statusEl.className = `status ${type}`; - statusEl.textContent = message; -} - -// Result display helper -function showResult( - elementId: string, - data: unknown, - additionalClass?: string -) { - const resultEl = document.getElementById(elementId)!; - resultEl.className = `result ${additionalClass || ""}`; - resultEl.innerHTML = `
${JSON.stringify(data, null, 2)}
`; -} - -// Check if SDK is initialized -function requireSdk(): boolean { - if (!permis) { - log("SDK not initialized. Please configure and initialize first.", "error"); - return false; - } - return true; -} - -// Initialize SDK -async function initializeSdk() { - const apiUrl = (document.getElementById("apiUrl") as HTMLInputElement).value; - const token = (document.getElementById("apiKey") as HTMLInputElement).value; - const projectId = (document.getElementById("projectId") as HTMLInputElement) - .value; - const environmentId = ( - document.getElementById("environmentId") as HTMLInputElement - ).value; - - if (!token) { - setStatus("API Key is required", "error"); - log("Initialization failed: API Key is required", "error"); - return; - } - - try { - permis = new Permis({ - token, - apiUrl, - // Only pass projectId/environmentId if provided - ...(projectId && { projectId }), - ...(environmentId && { environmentId }), - debug: true, - }); - - // If projectId or environmentId not provided, fetch from API key scope - if (!projectId || !environmentId) { - setStatus("⏳ Fetching scope from API key...", "info"); - log( - "Project/Environment IDs not provided, fetching from API key scope...", - "info" - ); - - try { - const scope = await permis.getScope(); - log( - `Scope fetched - Project: ${scope.projectId}, Environment: ${scope.environmentId}`, - "success" - ); - - // Update the UI fields with fetched values - if (scope.projectId) { - (document.getElementById("projectId") as HTMLInputElement).value = - scope.projectId; - } - if (scope.environmentId) { - (document.getElementById("environmentId") as HTMLInputElement).value = - scope.environmentId; - } - } catch (scopeError) { - setStatus(`Failed to fetch scope: ${scopeError}`, "error"); - log(`Scope fetch error: ${scopeError}`, "error"); - return; - } - } - - setStatus("✅ SDK initialized successfully!", "success"); - log(`SDK initialized with API URL: ${apiUrl}`, "success"); - - const config = permis.getConfig(); - log( - `Project: ${config.projectId}, Environment: ${config.environmentId}`, - "info" - ); - } catch (error) { - setStatus(`Failed to initialize SDK: ${error}`, "error"); - log(`Initialization error: ${error}`, "error"); - } -} - -// Permission Check -async function checkPermission() { - if (!requireSdk()) return; - - const user = (document.getElementById("checkUser") as HTMLInputElement).value; - const action = (document.getElementById("checkAction") as HTMLInputElement) - .value; - const resource = ( - document.getElementById("checkResource") as HTMLInputElement - ).value; - const tenant = (document.getElementById("checkTenant") as HTMLInputElement) - .value; - - if (!user || !action || !resource) { - log( - "User, Action, and Resource are required for permission check", - "error" - ); - return; - } - - log( - `Checking permission: ${user} -> ${action} -> ${resource}${ - tenant ? ` (tenant: ${tenant})` : "" - }`, - "info" - ); - - try { - const result = await permis!.checkWithDetails({ - user, - action, - resource, - tenant: tenant || undefined, - }); - - const allowed = result.allowed; - showResult("checkResult", result, allowed ? "allowed" : "denied"); - - const badge = allowed - ? '✓ ALLOWED' - : '✗ DENIED'; - - document.getElementById( - "checkResult" - )!.innerHTML = `${badge}
${JSON.stringify(result, null, 2)}
`; - - log( - `Permission check result: ${allowed ? "ALLOWED" : "DENIED"}`, - allowed ? "success" : "warn" - ); - } catch (error) { - showResult("checkResult", { error: String(error) }); - log(`Permission check failed: ${error}`, "error"); - } -} - -// Users -async function listUsers() { - if (!requireSdk()) return; - - log("Fetching users list...", "info"); - - try { - const result = await permis!.api.users.list(); - showResult("usersResult", result); - log(`Loaded ${result.data?.length || 0} users`, "success"); - } catch (error) { - showResult("usersResult", { error: String(error) }); - log(`Failed to list users: ${error}`, "error"); - } -} - -async function createUser() { - if (!requireSdk()) return; - - const key = (document.getElementById("newUserKey") as HTMLInputElement).value; - const email = (document.getElementById("newUserEmail") as HTMLInputElement) - .value; - const firstName = ( - document.getElementById("newUserFirstName") as HTMLInputElement - ).value; - const lastName = ( - document.getElementById("newUserLastName") as HTMLInputElement - ).value; - - if (!key) { - log("User key is required", "error"); - return; - } - - log(`Creating user: ${key}`, "info"); - - try { - const result = await permis!.api.users.create({ - key, - email: email || undefined, - firstName: firstName || undefined, - lastName: lastName || undefined, - }); - showResult("usersResult", result); - log(`User created successfully: ${key}`, "success"); - document.getElementById("createUserForm")?.classList.add("hidden"); - } catch (error) { - showResult("usersResult", { error: String(error) }); - log(`Failed to create user: ${error}`, "error"); - } -} - -// Roles -async function listRoles() { - if (!requireSdk()) return; - - log("Fetching roles list...", "info"); - - try { - const result = await permis!.api.roles.list(); - showResult("rolesResult", result); - log(`Loaded ${result.data?.length || 0} roles`, "success"); - } catch (error) { - showResult("rolesResult", { error: String(error) }); - log(`Failed to list roles: ${error}`, "error"); - } -} - -async function createRole() { - if (!requireSdk()) return; - - const key = (document.getElementById("newRoleKey") as HTMLInputElement).value; - const name = (document.getElementById("newRoleName") as HTMLInputElement) - .value; - const description = ( - document.getElementById("newRoleDescription") as HTMLInputElement - ).value; - const permissionsStr = ( - document.getElementById("newRolePermissions") as HTMLInputElement - ).value; - - if (!key) { - log("Role key is required", "error"); - return; - } - - const permissions = permissionsStr - ? permissionsStr - .split(",") - .map((p) => p.trim()) - .filter(Boolean) - : undefined; - - log(`Creating role: ${key}`, "info"); - - try { - const result = await permis!.api.roles.create({ - key, - name: name || undefined, - description: description || undefined, - permissions, - }); - showResult("rolesResult", result); - log(`Role created successfully: ${key}`, "success"); - document.getElementById("createRoleForm")?.classList.add("hidden"); - } catch (error) { - showResult("rolesResult", { error: String(error) }); - log(`Failed to create role: ${error}`, "error"); - } -} - -// Tenants -async function listTenants() { - if (!requireSdk()) return; - - log("Fetching tenants list...", "info"); - - try { - const result = await permis!.api.tenants.list(); - showResult("tenantsResult", result); - log(`Loaded ${result.data?.length || 0} tenants`, "success"); - } catch (error) { - showResult("tenantsResult", { error: String(error) }); - log(`Failed to list tenants: ${error}`, "error"); - } -} - -async function createTenant() { - if (!requireSdk()) return; - - const key = (document.getElementById("newTenantKey") as HTMLInputElement) - .value; - const name = (document.getElementById("newTenantName") as HTMLInputElement) - .value; - const description = ( - document.getElementById("newTenantDescription") as HTMLInputElement - ).value; - - if (!key) { - log("Tenant key is required", "error"); - return; - } - - log(`Creating tenant: ${key}`, "info"); - - try { - const result = await permis!.api.tenants.create({ - key, - name: name || undefined, - description: description || undefined, - }); - showResult("tenantsResult", result); - log(`Tenant created successfully: ${key}`, "success"); - document.getElementById("createTenantForm")?.classList.add("hidden"); - } catch (error) { - showResult("tenantsResult", { error: String(error) }); - log(`Failed to create tenant: ${error}`, "error"); - } -} - -// Resources -async function listResources() { - if (!requireSdk()) return; - - log("Fetching resources list...", "info"); - - try { - const result = await permis!.api.resources.list(); - showResult("resourcesResult", result); - log(`Loaded ${result.data?.length || 0} resources`, "success"); - } catch (error) { - showResult("resourcesResult", { error: String(error) }); - log(`Failed to list resources: ${error}`, "error"); - } -} - -async function createResource() { - if (!requireSdk()) return; - - const key = (document.getElementById("newResourceKey") as HTMLInputElement) - .value; - const name = (document.getElementById("newResourceName") as HTMLInputElement) - .value; - const description = ( - document.getElementById("newResourceDescription") as HTMLInputElement - ).value; - const actionsStr = ( - document.getElementById("newResourceActions") as HTMLInputElement - ).value; - - if (!key) { - log("Resource key is required", "error"); - return; - } - - const actions = actionsStr - ? actionsStr - .split(",") - .map((a) => a.trim()) - .filter(Boolean) - : undefined; - - log(`Creating resource: ${key}`, "info"); - - try { - const result = await permis!.api.resources.create({ - key, - name: name || undefined, - description: description || undefined, - actions, - }); - showResult("resourcesResult", result); - log(`Resource created successfully: ${key}`, "success"); - document.getElementById("createResourceForm")?.classList.add("hidden"); - } catch (error) { - showResult("resourcesResult", { error: String(error) }); - log(`Failed to create resource: ${error}`, "error"); - } -} - -// Role Assignments -async function listAssignments() { - if (!requireSdk()) return; - - log("Fetching role assignments list...", "info"); - - try { - const result = await permis!.api.roleAssignments.list(); - showResult("assignmentsResult", result); - log(`Loaded ${result.data?.length || 0} role assignments`, "success"); - } catch (error) { - showResult("assignmentsResult", { error: String(error) }); - log(`Failed to list role assignments: ${error}`, "error"); - } -} - -async function assignRole() { - if (!requireSdk()) return; - - const user = (document.getElementById("assignUser") as HTMLInputElement) - .value; - const role = (document.getElementById("assignRole") as HTMLInputElement) - .value; - const tenant = (document.getElementById("assignTenant") as HTMLInputElement) - .value; - - if (!user || !role) { - log("User and Role are required", "error"); - return; - } - - log( - `Assigning role ${role} to user ${user}${ - tenant ? ` in tenant ${tenant}` : "" - }`, - "info" - ); - - try { - const result = await permis!.api.roleAssignments.assign({ - user, - role, - tenant: tenant || undefined, - }); - showResult("assignmentsResult", result); - log(`Role assigned successfully`, "success"); - document.getElementById("assignRoleForm")?.classList.add("hidden"); - } catch (error) { - showResult("assignmentsResult", { error: String(error) }); - log(`Failed to assign role: ${error}`, "error"); - } -} - -// Clear console -function clearConsole() { - document.getElementById("console")!.innerHTML = ""; - log("Console cleared", "info"); -} - -// Initialize on DOM ready -document.addEventListener("DOMContentLoaded", () => { - setupTabs(); - setupFormToggles(); - - // Event listeners - document.getElementById("initSdk")?.addEventListener("click", initializeSdk); - document - .getElementById("checkPermission") - ?.addEventListener("click", checkPermission); - document - .getElementById("clearConsole") - ?.addEventListener("click", clearConsole); - - // Users - document.getElementById("listUsers")?.addEventListener("click", listUsers); - document.getElementById("createUser")?.addEventListener("click", createUser); - - // Roles - document.getElementById("listRoles")?.addEventListener("click", listRoles); - document.getElementById("createRole")?.addEventListener("click", createRole); - - // Tenants - document - .getElementById("listTenants") - ?.addEventListener("click", listTenants); - document - .getElementById("createTenant") - ?.addEventListener("click", createTenant); - - // Resources - document - .getElementById("listResources") - ?.addEventListener("click", listResources); - document - .getElementById("createResource") - ?.addEventListener("click", createResource); - - // Role Assignments - document - .getElementById("listAssignments") - ?.addEventListener("click", listAssignments); - document - .getElementById("assignRoleBtn") - ?.addEventListener("click", assignRole); - - log( - "Permis.io SDK Demo loaded. Configure your API settings to get started.", - "info" - ); -}); +import { Permissio } from "permissio"; + +// Global SDK instance +let permissio: Permissio | null = null; + +// Console logging helper +function log( + message: string, + type: "log" | "info" | "success" | "error" | "warn" = "log" +) { + const consoleEl = document.getElementById("console")!; + const time = new Date().toLocaleTimeString(); + const entry = document.createElement("div"); + entry.className = `console-entry ${type}`; + entry.innerHTML = `[${time}]${escapeHtml(message)}`; + consoleEl.appendChild(entry); + consoleEl.scrollTop = consoleEl.scrollHeight; +} + +function escapeHtml(text: string): string { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +// Tab switching +function setupTabs() { + const tabs = document.querySelectorAll(".tab"); + tabs.forEach((tab) => { + tab.addEventListener("click", () => { + const tabName = (tab as HTMLElement).dataset.tab; + + // Update active tab + tabs.forEach((t) => t.classList.remove("active")); + tab.classList.add("active"); + + // Update active content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + document.getElementById(`tab-${tabName}`)?.classList.add("active"); + }); + }); +} + +// Show/hide forms +function setupFormToggles() { + // Users + document.getElementById("showCreateUser")?.addEventListener("click", () => { + document.getElementById("createUserForm")?.classList.remove("hidden"); + }); + document.getElementById("cancelCreateUser")?.addEventListener("click", () => { + document.getElementById("createUserForm")?.classList.add("hidden"); + }); + + // Roles + document.getElementById("showCreateRole")?.addEventListener("click", () => { + document.getElementById("createRoleForm")?.classList.remove("hidden"); + }); + document.getElementById("cancelCreateRole")?.addEventListener("click", () => { + document.getElementById("createRoleForm")?.classList.add("hidden"); + }); + + // Tenants + document.getElementById("showCreateTenant")?.addEventListener("click", () => { + document.getElementById("createTenantForm")?.classList.remove("hidden"); + }); + document + .getElementById("cancelCreateTenant") + ?.addEventListener("click", () => { + document.getElementById("createTenantForm")?.classList.add("hidden"); + }); + + // Resources + document + .getElementById("showCreateResource") + ?.addEventListener("click", () => { + document.getElementById("createResourceForm")?.classList.remove("hidden"); + }); + document + .getElementById("cancelCreateResource") + ?.addEventListener("click", () => { + document.getElementById("createResourceForm")?.classList.add("hidden"); + }); + + // Role Assignments + document.getElementById("showAssignRole")?.addEventListener("click", () => { + document.getElementById("assignRoleForm")?.classList.remove("hidden"); + }); + document.getElementById("cancelAssignRole")?.addEventListener("click", () => { + document.getElementById("assignRoleForm")?.classList.add("hidden"); + }); +} + +// SDK Status helper +function setStatus(message: string, type: "success" | "error" | "info") { + const statusEl = document.getElementById("sdkStatus")!; + statusEl.className = `status ${type}`; + statusEl.textContent = message; +} + +// Result display helper +function showResult( + elementId: string, + data: unknown, + additionalClass?: string +) { + const resultEl = document.getElementById(elementId)!; + resultEl.className = `result ${additionalClass || ""}`; + resultEl.innerHTML = `
${JSON.stringify(data, null, 2)}
`; +} + +// Check if SDK is initialized +function requireSdk(): boolean { + if (!permissio) { + log("SDK not initialized. Please configure and initialize first.", "error"); + return false; + } + return true; +} + +// Initialize SDK +async function initializeSdk() { + const apiUrl = (document.getElementById("apiUrl") as HTMLInputElement).value; + const token = (document.getElementById("apiKey") as HTMLInputElement).value; + const projectId = (document.getElementById("projectId") as HTMLInputElement) + .value; + const environmentId = ( + document.getElementById("environmentId") as HTMLInputElement + ).value; + + if (!token) { + setStatus("API Key is required", "error"); + log("Initialization failed: API Key is required", "error"); + return; + } + + try { + permissio = new Permissio({ + token, + apiUrl, + // Only pass projectId/environmentId if provided + ...(projectId && { projectId }), + ...(environmentId && { environmentId }), + debug: true, + }); + + // If projectId or environmentId not provided, fetch from API key scope + if (!projectId || !environmentId) { + setStatus("⏳ Fetching scope from API key...", "info"); + log( + "Project/Environment IDs not provided, fetching from API key scope...", + "info" + ); + + try { + const scope = await permissio.getScope(); + log( + `Scope fetched - Project: ${scope.projectId}, Environment: ${scope.environmentId}`, + "success" + ); + + // Update the UI fields with fetched values + if (scope.projectId) { + (document.getElementById("projectId") as HTMLInputElement).value = + scope.projectId; + } + if (scope.environmentId) { + (document.getElementById("environmentId") as HTMLInputElement).value = + scope.environmentId; + } + } catch (scopeError) { + setStatus(`Failed to fetch scope: ${scopeError}`, "error"); + log(`Scope fetch error: ${scopeError}`, "error"); + return; + } + } + + setStatus("✅ SDK initialized successfully!", "success"); + log(`SDK initialized with API URL: ${apiUrl}`, "success"); + + const config = permissio.getConfig(); + log( + `Project: ${config.projectId}, Environment: ${config.environmentId}`, + "info" + ); + } catch (error) { + setStatus(`Failed to initialize SDK: ${error}`, "error"); + log(`Initialization error: ${error}`, "error"); + } +} + +// Permission Check +async function checkPermission() { + if (!requireSdk()) return; + + const user = (document.getElementById("checkUser") as HTMLInputElement).value; + const action = (document.getElementById("checkAction") as HTMLInputElement) + .value; + const resource = ( + document.getElementById("checkResource") as HTMLInputElement + ).value; + const tenant = (document.getElementById("checkTenant") as HTMLInputElement) + .value; + + if (!user || !action || !resource) { + log( + "User, Action, and Resource are required for permission check", + "error" + ); + return; + } + + log( + `Checking permission: ${user} -> ${action} -> ${resource}${ + tenant ? ` (tenant: ${tenant})` : "" + }`, + "info" + ); + + try { + const result = await permissio!.checkWithDetails({ + user, + action, + resource, + tenant: tenant || undefined, + }); + + const allowed = result.allowed; + showResult("checkResult", result, allowed ? "allowed" : "denied"); + + const badge = allowed + ? '✓ ALLOWED' + : '✗ DENIED'; + + document.getElementById( + "checkResult" + )!.innerHTML = `${badge}
${JSON.stringify(result, null, 2)}
`; + + log( + `Permission check result: ${allowed ? "ALLOWED" : "DENIED"}`, + allowed ? "success" : "warn" + ); + } catch (error) { + showResult("checkResult", { error: String(error) }); + log(`Permission check failed: ${error}`, "error"); + } +} + +// Users +async function listUsers() { + if (!requireSdk()) return; + + log("Fetching users list...", "info"); + + try { + const result = await permissio!.api.users.list(); + showResult("usersResult", result); + log(`Loaded ${result.data?.length || 0} users`, "success"); + } catch (error) { + showResult("usersResult", { error: String(error) }); + log(`Failed to list users: ${error}`, "error"); + } +} + +async function createUser() { + if (!requireSdk()) return; + + const key = (document.getElementById("newUserKey") as HTMLInputElement).value; + const email = (document.getElementById("newUserEmail") as HTMLInputElement) + .value; + const firstName = ( + document.getElementById("newUserFirstName") as HTMLInputElement + ).value; + const lastName = ( + document.getElementById("newUserLastName") as HTMLInputElement + ).value; + + if (!key) { + log("User key is required", "error"); + return; + } + + log(`Creating user: ${key}`, "info"); + + try { + const result = await permissio!.api.users.create({ + key, + email: email || undefined, + firstName: firstName || undefined, + lastName: lastName || undefined, + }); + showResult("usersResult", result); + log(`User created successfully: ${key}`, "success"); + document.getElementById("createUserForm")?.classList.add("hidden"); + } catch (error) { + showResult("usersResult", { error: String(error) }); + log(`Failed to create user: ${error}`, "error"); + } +} + +// Roles +async function listRoles() { + if (!requireSdk()) return; + + log("Fetching roles list...", "info"); + + try { + const result = await permissio!.api.roles.list(); + showResult("rolesResult", result); + log(`Loaded ${result.data?.length || 0} roles`, "success"); + } catch (error) { + showResult("rolesResult", { error: String(error) }); + log(`Failed to list roles: ${error}`, "error"); + } +} + +async function createRole() { + if (!requireSdk()) return; + + const key = (document.getElementById("newRoleKey") as HTMLInputElement).value; + const name = (document.getElementById("newRoleName") as HTMLInputElement) + .value; + const description = ( + document.getElementById("newRoleDescription") as HTMLInputElement + ).value; + const permissionsStr = ( + document.getElementById("newRolePermissions") as HTMLInputElement + ).value; + + if (!key) { + log("Role key is required", "error"); + return; + } + + const permissions = permissionsStr + ? permissionsStr + .split(",") + .map((p) => p.trim()) + .filter(Boolean) + : undefined; + + log(`Creating role: ${key}`, "info"); + + try { + const result = await permissio!.api.roles.create({ + key, + name: name || undefined, + description: description || undefined, + permissions, + }); + showResult("rolesResult", result); + log(`Role created successfully: ${key}`, "success"); + document.getElementById("createRoleForm")?.classList.add("hidden"); + } catch (error) { + showResult("rolesResult", { error: String(error) }); + log(`Failed to create role: ${error}`, "error"); + } +} + +// Tenants +async function listTenants() { + if (!requireSdk()) return; + + log("Fetching tenants list...", "info"); + + try { + const result = await permissio!.api.tenants.list(); + showResult("tenantsResult", result); + log(`Loaded ${result.data?.length || 0} tenants`, "success"); + } catch (error) { + showResult("tenantsResult", { error: String(error) }); + log(`Failed to list tenants: ${error}`, "error"); + } +} + +async function createTenant() { + if (!requireSdk()) return; + + const key = (document.getElementById("newTenantKey") as HTMLInputElement) + .value; + const name = (document.getElementById("newTenantName") as HTMLInputElement) + .value; + const description = ( + document.getElementById("newTenantDescription") as HTMLInputElement + ).value; + + if (!key) { + log("Tenant key is required", "error"); + return; + } + + log(`Creating tenant: ${key}`, "info"); + + try { + const result = await permissio!.api.tenants.create({ + key, + name: name || undefined, + description: description || undefined, + }); + showResult("tenantsResult", result); + log(`Tenant created successfully: ${key}`, "success"); + document.getElementById("createTenantForm")?.classList.add("hidden"); + } catch (error) { + showResult("tenantsResult", { error: String(error) }); + log(`Failed to create tenant: ${error}`, "error"); + } +} + +// Resources +async function listResources() { + if (!requireSdk()) return; + + log("Fetching resources list...", "info"); + + try { + const result = await permissio!.api.resources.list(); + showResult("resourcesResult", result); + log(`Loaded ${result.data?.length || 0} resources`, "success"); + } catch (error) { + showResult("resourcesResult", { error: String(error) }); + log(`Failed to list resources: ${error}`, "error"); + } +} + +async function createResource() { + if (!requireSdk()) return; + + const key = (document.getElementById("newResourceKey") as HTMLInputElement) + .value; + const name = (document.getElementById("newResourceName") as HTMLInputElement) + .value; + const description = ( + document.getElementById("newResourceDescription") as HTMLInputElement + ).value; + const actionsStr = ( + document.getElementById("newResourceActions") as HTMLInputElement + ).value; + + if (!key) { + log("Resource key is required", "error"); + return; + } + + const actions = actionsStr + ? actionsStr + .split(",") + .map((a) => a.trim()) + .filter(Boolean) + : undefined; + + log(`Creating resource: ${key}`, "info"); + + try { + const result = await permissio!.api.resources.create({ + key, + name: name || undefined, + description: description || undefined, + actions, + }); + showResult("resourcesResult", result); + log(`Resource created successfully: ${key}`, "success"); + document.getElementById("createResourceForm")?.classList.add("hidden"); + } catch (error) { + showResult("resourcesResult", { error: String(error) }); + log(`Failed to create resource: ${error}`, "error"); + } +} + +// Role Assignments +async function listAssignments() { + if (!requireSdk()) return; + + log("Fetching role assignments list...", "info"); + + try { + const result = await permissio!.api.roleAssignments.list(); + showResult("assignmentsResult", result); + log(`Loaded ${result.data?.length || 0} role assignments`, "success"); + } catch (error) { + showResult("assignmentsResult", { error: String(error) }); + log(`Failed to list role assignments: ${error}`, "error"); + } +} + +async function assignRole() { + if (!requireSdk()) return; + + const user = (document.getElementById("assignUser") as HTMLInputElement) + .value; + const role = (document.getElementById("assignRole") as HTMLInputElement) + .value; + const tenant = (document.getElementById("assignTenant") as HTMLInputElement) + .value; + + if (!user || !role) { + log("User and Role are required", "error"); + return; + } + + log( + `Assigning role ${role} to user ${user}${ + tenant ? ` in tenant ${tenant}` : "" + }`, + "info" + ); + + try { + const result = await permissio!.api.roleAssignments.assign({ + user, + role, + tenant: tenant || undefined, + }); + showResult("assignmentsResult", result); + log(`Role assigned successfully`, "success"); + document.getElementById("assignRoleForm")?.classList.add("hidden"); + } catch (error) { + showResult("assignmentsResult", { error: String(error) }); + log(`Failed to assign role: ${error}`, "error"); + } +} + +// Clear console +function clearConsole() { + document.getElementById("console")!.innerHTML = ""; + log("Console cleared", "info"); +} + +// Initialize on DOM ready +document.addEventListener("DOMContentLoaded", () => { + setupTabs(); + setupFormToggles(); + + // Event listeners + document.getElementById("initSdk")?.addEventListener("click", initializeSdk); + document + .getElementById("checkPermission") + ?.addEventListener("click", checkPermission); + document + .getElementById("clearConsole") + ?.addEventListener("click", clearConsole); + + // Users + document.getElementById("listUsers")?.addEventListener("click", listUsers); + document.getElementById("createUser")?.addEventListener("click", createUser); + + // Roles + document.getElementById("listRoles")?.addEventListener("click", listRoles); + document.getElementById("createRole")?.addEventListener("click", createRole); + + // Tenants + document + .getElementById("listTenants") + ?.addEventListener("click", listTenants); + document + .getElementById("createTenant") + ?.addEventListener("click", createTenant); + + // Resources + document + .getElementById("listResources") + ?.addEventListener("click", listResources); + document + .getElementById("createResource") + ?.addEventListener("click", createResource); + + // Role Assignments + document + .getElementById("listAssignments") + ?.addEventListener("click", listAssignments); + document + .getElementById("assignRoleBtn") + ?.addEventListener("click", assignRole); + + log( + "Permissio.io SDK Demo loaded. Configure your API settings to get started.", + "info" + ); +}); diff --git a/examples/browser-demo/vite.config.ts b/examples/browser-demo/vite.config.ts index 3cefe8c..db8c5b0 100644 --- a/examples/browser-demo/vite.config.ts +++ b/examples/browser-demo/vite.config.ts @@ -1,11 +1,11 @@ -import { defineConfig } from "vite"; - -export default defineConfig({ - server: { - port: 3000, - open: true, - }, - build: { - outDir: "dist", - }, -}); +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { + port: 3000, + open: true, + }, + build: { + outDir: "dist", + }, +}); diff --git a/package-lock.json b/package-lock.json index 5fd16d6..f11128e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "permisio", + "name": "permissio", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "permisio", + "name": "permissio", "version": "1.0.0", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c716207..ff1dfe2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "permissio", "version": "1.0.0", - "description": "Permis.io Node.js SDK - Authorization as a service", + "description": "Permissio.io Node.js SDK - Authorization as a service", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", @@ -33,10 +33,10 @@ "rbac", "abac", "access-control", - "permisio", + "permissio", "sdk" ], - "author": "Permis.io", + "author": "Permissio.io", "license": "MIT", "repository": { "type": "git", @@ -45,7 +45,7 @@ "bugs": { "url": "https://github.com/permissio/permissio-node/issues" }, - "homepage": "https://permis.io", + "homepage": "https://permissio.io", "engines": { "node": ">=16.0.0" }, diff --git a/src/api/base.ts b/src/api/base.ts index 7178ebe..e0ba41d 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -1,299 +1,299 @@ -import axios, { - AxiosInstance, - AxiosError, - AxiosRequestConfig, - InternalAxiosRequestConfig, -} from "axios"; -import { IResolvedConfig } from "../config"; -import { IApiError } from "../types"; - -/** - * Custom error class for Permis.io API errors - */ -export class PermisApiError extends Error { - public readonly statusCode: number; - public readonly code?: string; - public readonly details?: Record; - public readonly originalError?: AxiosError; - - constructor(error: IApiError, originalError?: AxiosError) { - super(error.message); - this.name = "PermisApiError"; - this.statusCode = error.statusCode; - this.code = error.code; - this.details = error.details; - this.originalError = originalError; - - // Maintains proper stack trace for where error was thrown - if (Error.captureStackTrace) { - Error.captureStackTrace(this, PermisApiError); - } - } -} - -/** - * Logger interface for SDK logging - */ -export interface ILogger { - debug: (message: string, data?: unknown) => void; - info: (message: string, data?: unknown) => void; - warn: (message: string, data?: unknown) => void; - error: (message: string, data?: unknown) => void; -} - -/** - * Default console logger - */ -const defaultLogger: ILogger = { - debug: (message: string, data?: unknown) => { - console.debug(`[Permis.io SDK] ${message}`, data ?? ""); - }, - info: (message: string, data?: unknown) => { - console.info(`[Permis.io SDK] ${message}`, data ?? ""); - }, - warn: (message: string, data?: unknown) => { - console.warn(`[Permis.io SDK] ${message}`, data ?? ""); - }, - error: (message: string, data?: unknown) => { - console.error(`[Permis.io SDK] ${message}`, data ?? ""); - }, -}; - -/** - * Base API client with HTTP request handling - */ -export class BaseApiClient { - protected readonly client: AxiosInstance; - protected readonly config: IResolvedConfig; - protected readonly logger: ILogger; - - constructor(config: IResolvedConfig, logger?: ILogger) { - this.config = config; - this.logger = config.debug - ? logger ?? defaultLogger - : this.createNoopLogger(); - - this.client = axios.create({ - baseURL: config.apiUrl, - timeout: config.timeout, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${config.token}`, - ...config.customHeaders, - }, - }); - - this.setupInterceptors(); - } - - /** - * Create a no-op logger when debug is disabled - */ - private createNoopLogger(): ILogger { - const noop = () => {}; - return { debug: noop, info: noop, warn: noop, error: noop }; - } - - /** - * Setup request and response interceptors - */ - private setupInterceptors(): void { - // Request interceptor for logging - this.client.interceptors.request.use( - (config: InternalAxiosRequestConfig) => { - this.logger.debug( - `Request: ${config.method?.toUpperCase()} ${config.url}`, - { - params: config.params, - data: config.data, - } - ); - return config; - }, - (error: AxiosError) => { - this.logger.error("Request error", error.message); - return Promise.reject(error); - } - ); - - // Response interceptor for logging and error handling - this.client.interceptors.response.use( - (response) => { - this.logger.debug(`Response: ${response.status}`, { - url: response.config.url, - data: response.data, - }); - return response; - }, - async (error: AxiosError) => { - return this.handleError(error); - } - ); - } - - /** - * Handle API errors with retry logic - */ - private async handleError(error: AxiosError): Promise { - const config = error.config as AxiosRequestConfig & { - _retryCount?: number; - }; - - // Retry logic for certain status codes - const retryableStatuses = [408, 429, 500, 502, 503, 504]; - const shouldRetry = - config && - (config._retryCount ?? 0) < this.config.retryAttempts && - error.response && - retryableStatuses.includes(error.response.status); - - if (shouldRetry) { - config._retryCount = (config._retryCount ?? 0) + 1; - const delay = Math.min(1000 * Math.pow(2, config._retryCount), 10000); - - this.logger.warn(`Retrying request (attempt ${config._retryCount})`, { - url: config.url, - delay, - }); - - await this.sleep(delay); - return this.client.request(config); - } - - // Transform error to PermisApiError - const apiError = this.transformError(error); - this.logger.error("API Error", apiError); - - if (this.config.throwOnError) { - throw new PermisApiError(apiError, error); - } - - return Promise.reject(new PermisApiError(apiError, error)); - } - - /** - * Transform axios error to API error format - */ - private transformError(error: AxiosError): IApiError { - if (error.response) { - const data = error.response.data as Record | undefined; - return { - message: (data?.message as string) || error.message, - code: (data?.code as string) || "API_ERROR", - statusCode: error.response.status, - details: data, - }; - } - - if (error.request) { - return { - message: "Network error - no response received", - code: "NETWORK_ERROR", - statusCode: 0, - }; - } - - return { - message: error.message, - code: "REQUEST_SETUP_ERROR", - statusCode: 0, - }; - } - - /** - * Sleep utility for retry delays - */ - private sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - /** - * Build the API path with project and environment context - */ - protected buildPath(path: string): string { - const { projectId, environmentId } = this.config; - - // Debug logging for path building - if (this.config.debug) { - this.logger.debug(`Building path for: ${path}`, { - projectId, - environmentId, - hasScope: !!(projectId && environmentId), - }); - } - - // For schema endpoints (resources, roles, actions) - use /v1/schema/{project}/{env}/... - if (path.startsWith("/schema")) { - if (projectId && environmentId) { - const result = `/v1/schema/${projectId}/${environmentId}${path.replace( - "/schema", - "" - )}`; - if (this.config.debug) { - this.logger.debug(`Built schema path: ${result}`); - } - return result; - } - return `/v1${path}`; - } - - // For facts endpoints (users, tenants, role_assignments, check) - use /v1/facts/{project}/{env}/... - if (projectId && environmentId) { - const result = `/v1/facts/${projectId}/${environmentId}${path}`; - if (this.config.debug) { - this.logger.debug(`Built facts path: ${result}`); - } - return result; - } - - const result = `/v1${path}`; - if (this.config.debug) { - this.logger.debug(`Built fallback path (no scope): ${result}`); - } - return result; - } - - /** - * Make a GET request - */ - protected async httpGet( - path: string, - params?: Record - ): Promise { - const response = await this.client.get(this.buildPath(path), { params }); - return response.data; - } - - /** - * Make a POST request - */ - protected async httpPost(path: string, data?: unknown): Promise { - const response = await this.client.post(this.buildPath(path), data); - return response.data; - } - - /** - * Make a PUT request - */ - protected async httpPut(path: string, data?: unknown): Promise { - const response = await this.client.put(this.buildPath(path), data); - return response.data; - } - - /** - * Make a PATCH request - */ - protected async httpPatch(path: string, data?: unknown): Promise { - const response = await this.client.patch(this.buildPath(path), data); - return response.data; - } - - /** - * Make a DELETE request - */ - protected async httpDelete(path: string): Promise { - const response = await this.client.delete(this.buildPath(path)); - return response.data; - } -} +import axios, { + AxiosInstance, + AxiosError, + AxiosRequestConfig, + InternalAxiosRequestConfig, +} from "axios"; +import { IResolvedConfig } from "../config"; +import { IApiError } from "../types"; + +/** + * Custom error class for Permissio.io API errors + */ +export class PermissioApiError extends Error { + public readonly statusCode: number; + public readonly code?: string; + public readonly details?: Record; + public readonly originalError?: AxiosError; + + constructor(error: IApiError, originalError?: AxiosError) { + super(error.message); + this.name = "PermissioApiError"; + this.statusCode = error.statusCode; + this.code = error.code; + this.details = error.details; + this.originalError = originalError; + + // Maintains proper stack trace for where error was thrown + if (Error.captureStackTrace) { + Error.captureStackTrace(this, PermissioApiError); + } + } +} + +/** + * Logger interface for SDK logging + */ +export interface ILogger { + debug: (message: string, data?: unknown) => void; + info: (message: string, data?: unknown) => void; + warn: (message: string, data?: unknown) => void; + error: (message: string, data?: unknown) => void; +} + +/** + * Default console logger + */ +const defaultLogger: ILogger = { + debug: (message: string, data?: unknown) => { + console.debug(`[Permissio.io SDK] ${message}`, data ?? ""); + }, + info: (message: string, data?: unknown) => { + console.info(`[Permissio.io SDK] ${message}`, data ?? ""); + }, + warn: (message: string, data?: unknown) => { + console.warn(`[Permissio.io SDK] ${message}`, data ?? ""); + }, + error: (message: string, data?: unknown) => { + console.error(`[Permissio.io SDK] ${message}`, data ?? ""); + }, +}; + +/** + * Base API client with HTTP request handling + */ +export class BaseApiClient { + protected readonly client: AxiosInstance; + protected readonly config: IResolvedConfig; + protected readonly logger: ILogger; + + constructor(config: IResolvedConfig, logger?: ILogger) { + this.config = config; + this.logger = config.debug + ? logger ?? defaultLogger + : this.createNoopLogger(); + + this.client = axios.create({ + baseURL: config.apiUrl, + timeout: config.timeout, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${config.token}`, + ...config.customHeaders, + }, + }); + + this.setupInterceptors(); + } + + /** + * Create a no-op logger when debug is disabled + */ + private createNoopLogger(): ILogger { + const noop = () => {}; + return { debug: noop, info: noop, warn: noop, error: noop }; + } + + /** + * Setup request and response interceptors + */ + private setupInterceptors(): void { + // Request interceptor for logging + this.client.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + this.logger.debug( + `Request: ${config.method?.toUpperCase()} ${config.url}`, + { + params: config.params, + data: config.data, + } + ); + return config; + }, + (error: AxiosError) => { + this.logger.error("Request error", error.message); + return Promise.reject(error); + } + ); + + // Response interceptor for logging and error handling + this.client.interceptors.response.use( + (response) => { + this.logger.debug(`Response: ${response.status}`, { + url: response.config.url, + data: response.data, + }); + return response; + }, + async (error: AxiosError) => { + return this.handleError(error); + } + ); + } + + /** + * Handle API errors with retry logic + */ + private async handleError(error: AxiosError): Promise { + const config = error.config as AxiosRequestConfig & { + _retryCount?: number; + }; + + // Retry logic for certain status codes + const retryableStatuses = [408, 429, 500, 502, 503, 504]; + const shouldRetry = + config && + (config._retryCount ?? 0) < this.config.retryAttempts && + error.response && + retryableStatuses.includes(error.response.status); + + if (shouldRetry) { + config._retryCount = (config._retryCount ?? 0) + 1; + const delay = Math.min(1000 * Math.pow(2, config._retryCount), 10000); + + this.logger.warn(`Retrying request (attempt ${config._retryCount})`, { + url: config.url, + delay, + }); + + await this.sleep(delay); + return this.client.request(config); + } + + // Transform error to PermissioApiError + const apiError = this.transformError(error); + this.logger.error("API Error", apiError); + + if (this.config.throwOnError) { + throw new PermissioApiError(apiError, error); + } + + return Promise.reject(new PermissioApiError(apiError, error)); + } + + /** + * Transform axios error to API error format + */ + private transformError(error: AxiosError): IApiError { + if (error.response) { + const data = error.response.data as Record | undefined; + return { + message: (data?.message as string) || error.message, + code: (data?.code as string) || "API_ERROR", + statusCode: error.response.status, + details: data, + }; + } + + if (error.request) { + return { + message: "Network error - no response received", + code: "NETWORK_ERROR", + statusCode: 0, + }; + } + + return { + message: error.message, + code: "REQUEST_SETUP_ERROR", + statusCode: 0, + }; + } + + /** + * Sleep utility for retry delays + */ + private sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Build the API path with project and environment context + */ + protected buildPath(path: string): string { + const { projectId, environmentId } = this.config; + + // Debug logging for path building + if (this.config.debug) { + this.logger.debug(`Building path for: ${path}`, { + projectId, + environmentId, + hasScope: !!(projectId && environmentId), + }); + } + + // For schema endpoints (resources, roles, actions) - use /v1/schema/{project}/{env}/... + if (path.startsWith("/schema")) { + if (projectId && environmentId) { + const result = `/v1/schema/${projectId}/${environmentId}${path.replace( + "/schema", + "" + )}`; + if (this.config.debug) { + this.logger.debug(`Built schema path: ${result}`); + } + return result; + } + return `/v1${path}`; + } + + // For facts endpoints (users, tenants, role_assignments, check) - use /v1/facts/{project}/{env}/... + if (projectId && environmentId) { + const result = `/v1/facts/${projectId}/${environmentId}${path}`; + if (this.config.debug) { + this.logger.debug(`Built facts path: ${result}`); + } + return result; + } + + const result = `/v1${path}`; + if (this.config.debug) { + this.logger.debug(`Built fallback path (no scope): ${result}`); + } + return result; + } + + /** + * Make a GET request + */ + protected async httpGet( + path: string, + params?: Record + ): Promise { + const response = await this.client.get(this.buildPath(path), { params }); + return response.data; + } + + /** + * Make a POST request + */ + protected async httpPost(path: string, data?: unknown): Promise { + const response = await this.client.post(this.buildPath(path), data); + return response.data; + } + + /** + * Make a PUT request + */ + protected async httpPut(path: string, data?: unknown): Promise { + const response = await this.client.put(this.buildPath(path), data); + return response.data; + } + + /** + * Make a PATCH request + */ + protected async httpPatch(path: string, data?: unknown): Promise { + const response = await this.client.patch(this.buildPath(path), data); + return response.data; + } + + /** + * Make a DELETE request + */ + protected async httpDelete(path: string): Promise { + const response = await this.client.delete(this.buildPath(path)); + return response.data; + } +} diff --git a/src/api/index.ts b/src/api/index.ts index bbe6d1c..4376e4e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,7 @@ -export { BaseApiClient, PermisApiError } from "./base"; -export type { ILogger } from "./base"; -export { UsersApi } from "./users"; -export { TenantsApi } from "./tenants"; -export { RolesApi } from "./roles"; -export { ResourcesApi } from "./resources"; -export { RoleAssignmentsApi } from "./role-assignments"; +export { BaseApiClient, PermissioApiError } from "./base"; +export type { ILogger } from "./base"; +export { UsersApi } from "./users"; +export { TenantsApi } from "./tenants"; +export { RolesApi } from "./roles"; +export { ResourcesApi } from "./resources"; +export { RoleAssignmentsApi } from "./role-assignments"; diff --git a/src/api/resources.ts b/src/api/resources.ts index 65e3502..d23d68c 100644 --- a/src/api/resources.ts +++ b/src/api/resources.ts @@ -1,190 +1,190 @@ -import { BaseApiClient } from "./base"; -import { - IResourceCreate, - IResourceUpdate, - IResourceRead, - IResourceList, - IResourceInstanceCreate, - IResourceInstanceRead, - IListParams, -} from "../types"; - -/** - * Resources API client for managing resource types and instances - */ -export class ResourcesApi extends BaseApiClient { - /** - * List all resource types - * @param params - Pagination and filtering parameters - */ - async list(params?: IListParams): Promise { - return this.httpGet( - "/schema/resources", - params as Record - ); - } - - /** - * Get a resource type by key - * @param resourceKey - The unique resource type key - */ - async get(resourceKey: string): Promise { - return this.httpGet( - `/schema/resources/${encodeURIComponent(resourceKey)}` - ); - } - - /** - * Create a new resource type - * @param resourceData - Resource type creation data - */ - async create(resourceData: IResourceCreate): Promise { - return this.httpPost("/schema/resources", resourceData); - } - - /** - * Update an existing resource type - * @param resourceKey - The unique resource type key - * @param resourceData - Resource type update data - */ - async update( - resourceKey: string, - resourceData: IResourceUpdate - ): Promise { - return this.httpPatch( - `/schema/resources/${encodeURIComponent(resourceKey)}`, - resourceData - ); - } - - /** - * Delete a resource type - * @param resourceKey - The unique resource type key - */ - async delete(resourceKey: string): Promise { - return this.httpDelete( - `/schema/resources/${encodeURIComponent(resourceKey)}` - ); - } - - /** - * Sync a resource type (create or update) - * @param resourceData - Resource type data to sync - */ - async sync(resourceData: IResourceCreate): Promise { - return this.httpPut( - `/schema/resources/${encodeURIComponent(resourceData.key)}`, - resourceData - ); - } - - /** - * Get actions for a resource type - * @param resourceKey - The unique resource type key - */ - async getActions(resourceKey: string): Promise { - const response = await this.httpGet<{ actions: string[] }>( - `/schema/resources/${encodeURIComponent(resourceKey)}/actions` - ); - return response.actions; - } - - /** - * Add an action to a resource type - * @param resourceKey - The unique resource type key - * @param action - The action to add - */ - async addAction(resourceKey: string, action: string): Promise { - return this.httpPost( - `/schema/resources/${encodeURIComponent(resourceKey)}/actions`, - { action } - ); - } - - /** - * Remove an action from a resource type - * @param resourceKey - The unique resource type key - * @param action - The action to remove - */ - async removeAction(resourceKey: string, action: string): Promise { - return this.httpDelete( - `/schema/resources/${encodeURIComponent( - resourceKey - )}/actions/${encodeURIComponent(action)}` - ); - } - - // ==================== Resource Instances ==================== - - /** - * List resource instances - * @param resourceType - The resource type key - * @param params - Pagination and filtering parameters - */ - async listInstances( - resourceType: string, - params?: IListParams - ): Promise<{ data: IResourceInstanceRead[]; total: number }> { - return this.httpGet( - `/resource_instances/${encodeURIComponent(resourceType)}`, - params as Record - ); - } - - /** - * Get a resource instance - * @param resourceType - The resource type key - * @param instanceKey - The instance key - */ - async getInstance( - resourceType: string, - instanceKey: string - ): Promise { - return this.httpGet( - `/resource_instances/${encodeURIComponent( - resourceType - )}/${encodeURIComponent(instanceKey)}` - ); - } - - /** - * Create a resource instance - * @param instanceData - Resource instance creation data - */ - async createInstance( - instanceData: IResourceInstanceCreate - ): Promise { - return this.httpPost("/resource_instances", instanceData); - } - - /** - * Delete a resource instance - * @param resourceType - The resource type key - * @param instanceKey - The instance key - */ - async deleteInstance( - resourceType: string, - instanceKey: string - ): Promise { - return this.httpDelete( - `/resource_instances/${encodeURIComponent( - resourceType - )}/${encodeURIComponent(instanceKey)}` - ); - } - - /** - * Sync a resource instance (create or update) - * @param instanceData - Resource instance data to sync - */ - async syncInstance( - instanceData: IResourceInstanceCreate - ): Promise { - return this.httpPut( - `/resource_instances/${encodeURIComponent( - instanceData.resourceType - )}/${encodeURIComponent(instanceData.key)}`, - instanceData - ); - } -} +import { BaseApiClient } from "./base"; +import { + IResourceCreate, + IResourceUpdate, + IResourceRead, + IResourceList, + IResourceInstanceCreate, + IResourceInstanceRead, + IListParams, +} from "../types"; + +/** + * Resources API client for managing resource types and instances + */ +export class ResourcesApi extends BaseApiClient { + /** + * List all resource types + * @param params - Pagination and filtering parameters + */ + async list(params?: IListParams): Promise { + return this.httpGet( + "/schema/resources", + params as Record + ); + } + + /** + * Get a resource type by key + * @param resourceKey - The unique resource type key + */ + async get(resourceKey: string): Promise { + return this.httpGet( + `/schema/resources/${encodeURIComponent(resourceKey)}` + ); + } + + /** + * Create a new resource type + * @param resourceData - Resource type creation data + */ + async create(resourceData: IResourceCreate): Promise { + return this.httpPost("/schema/resources", resourceData); + } + + /** + * Update an existing resource type + * @param resourceKey - The unique resource type key + * @param resourceData - Resource type update data + */ + async update( + resourceKey: string, + resourceData: IResourceUpdate + ): Promise { + return this.httpPatch( + `/schema/resources/${encodeURIComponent(resourceKey)}`, + resourceData + ); + } + + /** + * Delete a resource type + * @param resourceKey - The unique resource type key + */ + async delete(resourceKey: string): Promise { + return this.httpDelete( + `/schema/resources/${encodeURIComponent(resourceKey)}` + ); + } + + /** + * Sync a resource type (create or update) + * @param resourceData - Resource type data to sync + */ + async sync(resourceData: IResourceCreate): Promise { + return this.httpPut( + `/schema/resources/${encodeURIComponent(resourceData.key)}`, + resourceData + ); + } + + /** + * Get actions for a resource type + * @param resourceKey - The unique resource type key + */ + async getActions(resourceKey: string): Promise { + const response = await this.httpGet<{ actions: string[] }>( + `/schema/resources/${encodeURIComponent(resourceKey)}/actions` + ); + return response.actions; + } + + /** + * Add an action to a resource type + * @param resourceKey - The unique resource type key + * @param action - The action to add + */ + async addAction(resourceKey: string, action: string): Promise { + return this.httpPost( + `/schema/resources/${encodeURIComponent(resourceKey)}/actions`, + { action } + ); + } + + /** + * Remove an action from a resource type + * @param resourceKey - The unique resource type key + * @param action - The action to remove + */ + async removeAction(resourceKey: string, action: string): Promise { + return this.httpDelete( + `/schema/resources/${encodeURIComponent( + resourceKey + )}/actions/${encodeURIComponent(action)}` + ); + } + + // ==================== Resource Instances ==================== + + /** + * List resource instances + * @param resourceType - The resource type key + * @param params - Pagination and filtering parameters + */ + async listInstances( + resourceType: string, + params?: IListParams + ): Promise<{ data: IResourceInstanceRead[]; total: number }> { + return this.httpGet( + `/resource_instances/${encodeURIComponent(resourceType)}`, + params as Record + ); + } + + /** + * Get a resource instance + * @param resourceType - The resource type key + * @param instanceKey - The instance key + */ + async getInstance( + resourceType: string, + instanceKey: string + ): Promise { + return this.httpGet( + `/resource_instances/${encodeURIComponent( + resourceType + )}/${encodeURIComponent(instanceKey)}` + ); + } + + /** + * Create a resource instance + * @param instanceData - Resource instance creation data + */ + async createInstance( + instanceData: IResourceInstanceCreate + ): Promise { + return this.httpPost("/resource_instances", instanceData); + } + + /** + * Delete a resource instance + * @param resourceType - The resource type key + * @param instanceKey - The instance key + */ + async deleteInstance( + resourceType: string, + instanceKey: string + ): Promise { + return this.httpDelete( + `/resource_instances/${encodeURIComponent( + resourceType + )}/${encodeURIComponent(instanceKey)}` + ); + } + + /** + * Sync a resource instance (create or update) + * @param instanceData - Resource instance data to sync + */ + async syncInstance( + instanceData: IResourceInstanceCreate + ): Promise { + return this.httpPut( + `/resource_instances/${encodeURIComponent( + instanceData.resourceType + )}/${encodeURIComponent(instanceData.key)}`, + instanceData + ); + } +} diff --git a/src/api/role-assignments.ts b/src/api/role-assignments.ts index 2d09adb..8bb0981 100644 --- a/src/api/role-assignments.ts +++ b/src/api/role-assignments.ts @@ -1,237 +1,237 @@ -import { BaseApiClient } from "./base"; -import { - IRoleAssignmentCreate, - IRoleAssignmentRead, - IRoleAssignmentRemove, - IRoleAssignmentList, - IBulkRoleAssignmentResponse, - IListParams, -} from "../types"; - -/** - * Role Assignments API client for managing user role assignments - */ -export class RoleAssignmentsApi extends BaseApiClient { - /** - * Normalize the response to always have a data property - * Backend may return array directly or { data: [...] } - */ - private normalizeListResponse( - response: IRoleAssignmentRead[] | IRoleAssignmentList - ): IRoleAssignmentList { - // If it's already the expected format with data property - if (response && typeof response === "object" && "data" in response) { - return response as IRoleAssignmentList; - } - // If it's a direct array, wrap it - if (Array.isArray(response)) { - return { - data: response, - page: 1, - perPage: response.length, - total: response.length, - totalPages: 1, - }; - } - // Fallback - return { data: [], page: 1, perPage: 0, total: 0, totalPages: 0 }; - } - - /** - * List all role assignments - * @param params - Pagination and filtering parameters - */ - async list( - params?: IListParams & { - user?: string; - role?: string; - tenant?: string; - resource?: string; - } - ): Promise { - const response = await this.httpGet< - IRoleAssignmentRead[] | IRoleAssignmentList - >("/role_assignments", params as Record); - return this.normalizeListResponse(response); - } - - /** - * Get role assignments for a specific user - * @param userKey - The user key - * @param params - Additional filtering parameters - */ - async listByUser( - userKey: string, - params?: { tenant?: string; resource?: string } - ): Promise { - const response = await this.httpGet< - IRoleAssignmentRead[] | IRoleAssignmentList - >("/role_assignments", { - user: userKey, - ...params, - }); - return this.normalizeListResponse(response); - } - - /** - * Get role assignments for a specific tenant - * @param tenantKey - The tenant key - * @param params - Additional filtering parameters - */ - async listByTenant( - tenantKey: string, - params?: { role?: string } - ): Promise { - const response = await this.httpGet< - IRoleAssignmentRead[] | IRoleAssignmentList - >("/role_assignments", { - tenant: tenantKey, - ...params, - }); - return this.normalizeListResponse(response); - } - - /** - * Get role assignments for a specific resource - * @param resourceType - The resource type key - * @param resourceInstance - Optional resource instance key - */ - async listByResource( - resourceType: string, - resourceInstance?: string - ): Promise { - const response = await this.httpGet< - IRoleAssignmentRead[] | IRoleAssignmentList - >("/role_assignments", { - resource: resourceType, - resourceInstance, - }); - return this.normalizeListResponse(response); - } - - /** - * Create a role assignment - * @param assignment - Role assignment data - */ - async assign( - assignment: IRoleAssignmentCreate - ): Promise { - return this.httpPost("/role_assignments", assignment); - } - - /** - * Remove a role assignment - * @param assignment - Role assignment to remove - */ - async unassign(assignment: IRoleAssignmentRemove): Promise { - const params = new URLSearchParams(); - params.append("user", assignment.user); - params.append("role", assignment.role); - if (assignment.tenant) params.append("tenant", assignment.tenant); - if (assignment.resource) params.append("resource", assignment.resource); - if (assignment.resourceInstance) - params.append("resourceInstance", assignment.resourceInstance); - - return this.httpDelete(`/role_assignments?${params.toString()}`); - } - - /** - * Bulk create role assignments - * @param assignments - Array of role assignments to create - */ - async bulkAssign( - assignments: IRoleAssignmentCreate[] - ): Promise { - return this.httpPost( - "/role_assignments/bulk", - { - assignments, - } - ); - } - - /** - * Bulk remove role assignments - * @param assignments - Array of role assignments to remove - */ - async bulkUnassign( - assignments: IRoleAssignmentRemove[] - ): Promise { - return this.httpPost( - "/role_assignments/bulk/delete", - { - assignments, - } - ); - } - - /** - * Check if a user has a specific role - * @param userKey - The user key - * @param roleKey - The role key - * @param options - Optional tenant/resource context - */ - async hasRole( - userKey: string, - roleKey: string, - options?: { tenant?: string; resource?: string; resourceInstance?: string } - ): Promise { - const params: Record = { - user: userKey, - role: roleKey, - }; - if (options?.tenant) params.tenant = options.tenant; - if (options?.resource) params.resource = options.resource; - if (options?.resourceInstance) - params.resourceInstance = options.resourceInstance; - - try { - const response = await this.httpGet<{ hasRole: boolean }>( - "/role_assignments/check", - params - ); - return response.hasRole; - } catch { - return false; - } - } - - /** - * Get all roles assigned to a user - * @param userKey - The user key - * @param options - Optional tenant/resource context - */ - async getUserRoles( - userKey: string, - options?: { tenant?: string; resource?: string } - ): Promise { - const params: Record = { user: userKey }; - if (options?.tenant) params.tenant = options.tenant; - if (options?.resource) params.resource = options.resource; - - const response = await this.httpGet<{ roles: string[] }>( - "/role_assignments/roles", - params - ); - return response.roles; - } - - /** - * Get all users with a specific role - * @param roleKey - The role key - * @param options - Optional tenant context - */ - async getRoleUsers( - roleKey: string, - options?: { tenant?: string } - ): Promise { - const params: Record = { role: roleKey }; - if (options?.tenant) params.tenant = options.tenant; - - const response = await this.httpGet<{ users: string[] }>( - "/role_assignments/users", - params - ); - return response.users; - } -} +import { BaseApiClient } from "./base"; +import { + IRoleAssignmentCreate, + IRoleAssignmentRead, + IRoleAssignmentRemove, + IRoleAssignmentList, + IBulkRoleAssignmentResponse, + IListParams, +} from "../types"; + +/** + * Role Assignments API client for managing user role assignments + */ +export class RoleAssignmentsApi extends BaseApiClient { + /** + * Normalize the response to always have a data property + * Backend may return array directly or { data: [...] } + */ + private normalizeListResponse( + response: IRoleAssignmentRead[] | IRoleAssignmentList + ): IRoleAssignmentList { + // If it's already the expected format with data property + if (response && typeof response === "object" && "data" in response) { + return response as IRoleAssignmentList; + } + // If it's a direct array, wrap it + if (Array.isArray(response)) { + return { + data: response, + page: 1, + perPage: response.length, + total: response.length, + totalPages: 1, + }; + } + // Fallback + return { data: [], page: 1, perPage: 0, total: 0, totalPages: 0 }; + } + + /** + * List all role assignments + * @param params - Pagination and filtering parameters + */ + async list( + params?: IListParams & { + user?: string; + role?: string; + tenant?: string; + resource?: string; + } + ): Promise { + const response = await this.httpGet< + IRoleAssignmentRead[] | IRoleAssignmentList + >("/role_assignments", params as Record); + return this.normalizeListResponse(response); + } + + /** + * Get role assignments for a specific user + * @param userKey - The user key + * @param params - Additional filtering parameters + */ + async listByUser( + userKey: string, + params?: { tenant?: string; resource?: string } + ): Promise { + const response = await this.httpGet< + IRoleAssignmentRead[] | IRoleAssignmentList + >("/role_assignments", { + user: userKey, + ...params, + }); + return this.normalizeListResponse(response); + } + + /** + * Get role assignments for a specific tenant + * @param tenantKey - The tenant key + * @param params - Additional filtering parameters + */ + async listByTenant( + tenantKey: string, + params?: { role?: string } + ): Promise { + const response = await this.httpGet< + IRoleAssignmentRead[] | IRoleAssignmentList + >("/role_assignments", { + tenant: tenantKey, + ...params, + }); + return this.normalizeListResponse(response); + } + + /** + * Get role assignments for a specific resource + * @param resourceType - The resource type key + * @param resourceInstance - Optional resource instance key + */ + async listByResource( + resourceType: string, + resourceInstance?: string + ): Promise { + const response = await this.httpGet< + IRoleAssignmentRead[] | IRoleAssignmentList + >("/role_assignments", { + resource: resourceType, + resourceInstance, + }); + return this.normalizeListResponse(response); + } + + /** + * Create a role assignment + * @param assignment - Role assignment data + */ + async assign( + assignment: IRoleAssignmentCreate + ): Promise { + return this.httpPost("/role_assignments", assignment); + } + + /** + * Remove a role assignment + * @param assignment - Role assignment to remove + */ + async unassign(assignment: IRoleAssignmentRemove): Promise { + const params = new URLSearchParams(); + params.append("user", assignment.user); + params.append("role", assignment.role); + if (assignment.tenant) params.append("tenant", assignment.tenant); + if (assignment.resource) params.append("resource", assignment.resource); + if (assignment.resourceInstance) + params.append("resourceInstance", assignment.resourceInstance); + + return this.httpDelete(`/role_assignments?${params.toString()}`); + } + + /** + * Bulk create role assignments + * @param assignments - Array of role assignments to create + */ + async bulkAssign( + assignments: IRoleAssignmentCreate[] + ): Promise { + return this.httpPost( + "/role_assignments/bulk", + { + assignments, + } + ); + } + + /** + * Bulk remove role assignments + * @param assignments - Array of role assignments to remove + */ + async bulkUnassign( + assignments: IRoleAssignmentRemove[] + ): Promise { + return this.httpPost( + "/role_assignments/bulk/delete", + { + assignments, + } + ); + } + + /** + * Check if a user has a specific role + * @param userKey - The user key + * @param roleKey - The role key + * @param options - Optional tenant/resource context + */ + async hasRole( + userKey: string, + roleKey: string, + options?: { tenant?: string; resource?: string; resourceInstance?: string } + ): Promise { + const params: Record = { + user: userKey, + role: roleKey, + }; + if (options?.tenant) params.tenant = options.tenant; + if (options?.resource) params.resource = options.resource; + if (options?.resourceInstance) + params.resourceInstance = options.resourceInstance; + + try { + const response = await this.httpGet<{ hasRole: boolean }>( + "/role_assignments/check", + params + ); + return response.hasRole; + } catch { + return false; + } + } + + /** + * Get all roles assigned to a user + * @param userKey - The user key + * @param options - Optional tenant/resource context + */ + async getUserRoles( + userKey: string, + options?: { tenant?: string; resource?: string } + ): Promise { + const params: Record = { user: userKey }; + if (options?.tenant) params.tenant = options.tenant; + if (options?.resource) params.resource = options.resource; + + const response = await this.httpGet<{ roles: string[] }>( + "/role_assignments/roles", + params + ); + return response.roles; + } + + /** + * Get all users with a specific role + * @param roleKey - The role key + * @param options - Optional tenant context + */ + async getRoleUsers( + roleKey: string, + options?: { tenant?: string } + ): Promise { + const params: Record = { role: roleKey }; + if (options?.tenant) params.tenant = options.tenant; + + const response = await this.httpGet<{ users: string[] }>( + "/role_assignments/users", + params + ); + return response.users; + } +} diff --git a/src/api/roles.ts b/src/api/roles.ts index 639a0c7..f037c38 100644 --- a/src/api/roles.ts +++ b/src/api/roles.ts @@ -1,147 +1,147 @@ -import { BaseApiClient } from "./base"; -import { - IRoleCreate, - IRoleUpdate, - IRoleRead, - IRoleList, - IListParams, -} from "../types"; - -/** - * Roles API client for managing roles in the authorization system - */ -export class RolesApi extends BaseApiClient { - /** - * List all roles - * @param params - Pagination and filtering parameters - */ - async list(params?: IListParams): Promise { - return this.httpGet( - "/schema/roles", - params as Record - ); - } - - /** - * Get a role by key - * @param roleKey - The unique role key - */ - async get(roleKey: string): Promise { - return this.httpGet( - `/schema/roles/${encodeURIComponent(roleKey)}` - ); - } - - /** - * Create a new role - * @param roleData - Role creation data - */ - async create(roleData: IRoleCreate): Promise { - return this.httpPost("/schema/roles", roleData); - } - - /** - * Update an existing role - * @param roleKey - The unique role key - * @param roleData - Role update data - */ - async update(roleKey: string, roleData: IRoleUpdate): Promise { - return this.httpPatch( - `/schema/roles/${encodeURIComponent(roleKey)}`, - roleData - ); - } - - /** - * Delete a role - * @param roleKey - The unique role key - */ - async delete(roleKey: string): Promise { - return this.httpDelete(`/schema/roles/${encodeURIComponent(roleKey)}`); - } - - /** - * Sync a role (create or update) - * @param roleData - Role data to sync - */ - async sync(roleData: IRoleCreate): Promise { - return this.httpPut( - `/schema/roles/${encodeURIComponent(roleData.key)}`, - roleData - ); - } - - /** - * Get permissions for a role - * @param roleKey - The unique role key - */ - async getPermissions(roleKey: string): Promise { - const response = await this.httpGet<{ permissions: string[] }>( - `/schema/roles/${encodeURIComponent(roleKey)}/permissions` - ); - return response.permissions; - } - - /** - * Add a permission to a role - * @param roleKey - The unique role key - * @param permission - The permission to add (format: "resource:action") - */ - async addPermission(roleKey: string, permission: string): Promise { - return this.httpPost( - `/schema/roles/${encodeURIComponent(roleKey)}/permissions`, - { permission } - ); - } - - /** - * Remove a permission from a role - * @param roleKey - The unique role key - * @param permission - The permission to remove - */ - async removePermission(roleKey: string, permission: string): Promise { - return this.httpDelete( - `/schema/roles/${encodeURIComponent( - roleKey - )}/permissions/${encodeURIComponent(permission)}` - ); - } - - /** - * Get roles that this role extends - * @param roleKey - The unique role key - */ - async getExtends(roleKey: string): Promise { - const response = await this.httpGet<{ extends: string[] }>( - `/schema/roles/${encodeURIComponent(roleKey)}/extends` - ); - return response.extends; - } - - /** - * Add a parent role (role inheritance) - * @param roleKey - The unique role key - * @param parentRoleKey - The parent role key to extend - */ - async addExtends(roleKey: string, parentRoleKey: string): Promise { - return this.httpPost( - `/schema/roles/${encodeURIComponent(roleKey)}/extends`, - { - role: parentRoleKey, - } - ); - } - - /** - * Remove a parent role - * @param roleKey - The unique role key - * @param parentRoleKey - The parent role key to remove - */ - async removeExtends(roleKey: string, parentRoleKey: string): Promise { - return this.httpDelete( - `/schema/roles/${encodeURIComponent( - roleKey - )}/extends/${encodeURIComponent(parentRoleKey)}` - ); - } -} +import { BaseApiClient } from "./base"; +import { + IRoleCreate, + IRoleUpdate, + IRoleRead, + IRoleList, + IListParams, +} from "../types"; + +/** + * Roles API client for managing roles in the authorization system + */ +export class RolesApi extends BaseApiClient { + /** + * List all roles + * @param params - Pagination and filtering parameters + */ + async list(params?: IListParams): Promise { + return this.httpGet( + "/schema/roles", + params as Record + ); + } + + /** + * Get a role by key + * @param roleKey - The unique role key + */ + async get(roleKey: string): Promise { + return this.httpGet( + `/schema/roles/${encodeURIComponent(roleKey)}` + ); + } + + /** + * Create a new role + * @param roleData - Role creation data + */ + async create(roleData: IRoleCreate): Promise { + return this.httpPost("/schema/roles", roleData); + } + + /** + * Update an existing role + * @param roleKey - The unique role key + * @param roleData - Role update data + */ + async update(roleKey: string, roleData: IRoleUpdate): Promise { + return this.httpPatch( + `/schema/roles/${encodeURIComponent(roleKey)}`, + roleData + ); + } + + /** + * Delete a role + * @param roleKey - The unique role key + */ + async delete(roleKey: string): Promise { + return this.httpDelete(`/schema/roles/${encodeURIComponent(roleKey)}`); + } + + /** + * Sync a role (create or update) + * @param roleData - Role data to sync + */ + async sync(roleData: IRoleCreate): Promise { + return this.httpPut( + `/schema/roles/${encodeURIComponent(roleData.key)}`, + roleData + ); + } + + /** + * Get permissions for a role + * @param roleKey - The unique role key + */ + async getPermissions(roleKey: string): Promise { + const response = await this.httpGet<{ permissions: string[] }>( + `/schema/roles/${encodeURIComponent(roleKey)}/permissions` + ); + return response.permissions; + } + + /** + * Add a permission to a role + * @param roleKey - The unique role key + * @param permission - The permission to add (format: "resource:action") + */ + async addPermission(roleKey: string, permission: string): Promise { + return this.httpPost( + `/schema/roles/${encodeURIComponent(roleKey)}/permissions`, + { permission } + ); + } + + /** + * Remove a permission from a role + * @param roleKey - The unique role key + * @param permission - The permission to remove + */ + async removePermission(roleKey: string, permission: string): Promise { + return this.httpDelete( + `/schema/roles/${encodeURIComponent( + roleKey + )}/permissions/${encodeURIComponent(permission)}` + ); + } + + /** + * Get roles that this role extends + * @param roleKey - The unique role key + */ + async getExtends(roleKey: string): Promise { + const response = await this.httpGet<{ extends: string[] }>( + `/schema/roles/${encodeURIComponent(roleKey)}/extends` + ); + return response.extends; + } + + /** + * Add a parent role (role inheritance) + * @param roleKey - The unique role key + * @param parentRoleKey - The parent role key to extend + */ + async addExtends(roleKey: string, parentRoleKey: string): Promise { + return this.httpPost( + `/schema/roles/${encodeURIComponent(roleKey)}/extends`, + { + role: parentRoleKey, + } + ); + } + + /** + * Remove a parent role + * @param roleKey - The unique role key + * @param parentRoleKey - The parent role key to remove + */ + async removeExtends(roleKey: string, parentRoleKey: string): Promise { + return this.httpDelete( + `/schema/roles/${encodeURIComponent( + roleKey + )}/extends/${encodeURIComponent(parentRoleKey)}` + ); + } +} diff --git a/src/api/tenants.ts b/src/api/tenants.ts index 42a33a5..931f3d8 100644 --- a/src/api/tenants.ts +++ b/src/api/tenants.ts @@ -1,111 +1,111 @@ -import { BaseApiClient } from "./base"; -import { - ITenantCreate, - ITenantUpdate, - ITenantRead, - ITenantList, - IListParams, -} from "../types"; - -/** - * Tenants API client for managing tenants (multi-tenancy support) - */ -export class TenantsApi extends BaseApiClient { - /** - * List all tenants - * @param params - Pagination and filtering parameters - */ - async list(params?: IListParams): Promise { - return this.httpGet( - "/tenants", - params as Record - ); - } - - /** - * Get a tenant by key - * @param tenantKey - The unique tenant key - */ - async get(tenantKey: string): Promise { - return this.httpGet( - `/tenants/${encodeURIComponent(tenantKey)}` - ); - } - - /** - * Create a new tenant - * @param tenantData - Tenant creation data - */ - async create(tenantData: ITenantCreate): Promise { - return this.httpPost("/tenants", tenantData); - } - - /** - * Update an existing tenant - * @param tenantKey - The unique tenant key - * @param tenantData - Tenant update data - */ - async update( - tenantKey: string, - tenantData: ITenantUpdate - ): Promise { - return this.httpPatch( - `/tenants/${encodeURIComponent(tenantKey)}`, - tenantData - ); - } - - /** - * Delete a tenant - * @param tenantKey - The unique tenant key - */ - async delete(tenantKey: string): Promise { - return this.httpDelete(`/tenants/${encodeURIComponent(tenantKey)}`); - } - - /** - * Sync a tenant (create or update) - * @param tenantData - Tenant data to sync - */ - async sync(tenantData: ITenantCreate): Promise { - return this.httpPut( - `/tenants/${encodeURIComponent(tenantData.key)}`, - tenantData - ); - } - - /** - * Get all users in a tenant - * @param tenantKey - The unique tenant key - */ - async getUsers(tenantKey: string): Promise { - const response = await this.httpGet<{ users: string[] }>( - `/tenants/${encodeURIComponent(tenantKey)}/users` - ); - return response.users; - } - - /** - * Add a user to a tenant - * @param tenantKey - The unique tenant key - * @param userKey - The user key to add - */ - async addUser(tenantKey: string, userKey: string): Promise { - return this.httpPost(`/tenants/${encodeURIComponent(tenantKey)}/users`, { - user: userKey, - }); - } - - /** - * Remove a user from a tenant - * @param tenantKey - The unique tenant key - * @param userKey - The user key to remove - */ - async removeUser(tenantKey: string, userKey: string): Promise { - return this.httpDelete( - `/tenants/${encodeURIComponent(tenantKey)}/users/${encodeURIComponent( - userKey - )}` - ); - } -} +import { BaseApiClient } from "./base"; +import { + ITenantCreate, + ITenantUpdate, + ITenantRead, + ITenantList, + IListParams, +} from "../types"; + +/** + * Tenants API client for managing tenants (multi-tenancy support) + */ +export class TenantsApi extends BaseApiClient { + /** + * List all tenants + * @param params - Pagination and filtering parameters + */ + async list(params?: IListParams): Promise { + return this.httpGet( + "/tenants", + params as Record + ); + } + + /** + * Get a tenant by key + * @param tenantKey - The unique tenant key + */ + async get(tenantKey: string): Promise { + return this.httpGet( + `/tenants/${encodeURIComponent(tenantKey)}` + ); + } + + /** + * Create a new tenant + * @param tenantData - Tenant creation data + */ + async create(tenantData: ITenantCreate): Promise { + return this.httpPost("/tenants", tenantData); + } + + /** + * Update an existing tenant + * @param tenantKey - The unique tenant key + * @param tenantData - Tenant update data + */ + async update( + tenantKey: string, + tenantData: ITenantUpdate + ): Promise { + return this.httpPatch( + `/tenants/${encodeURIComponent(tenantKey)}`, + tenantData + ); + } + + /** + * Delete a tenant + * @param tenantKey - The unique tenant key + */ + async delete(tenantKey: string): Promise { + return this.httpDelete(`/tenants/${encodeURIComponent(tenantKey)}`); + } + + /** + * Sync a tenant (create or update) + * @param tenantData - Tenant data to sync + */ + async sync(tenantData: ITenantCreate): Promise { + return this.httpPut( + `/tenants/${encodeURIComponent(tenantData.key)}`, + tenantData + ); + } + + /** + * Get all users in a tenant + * @param tenantKey - The unique tenant key + */ + async getUsers(tenantKey: string): Promise { + const response = await this.httpGet<{ users: string[] }>( + `/tenants/${encodeURIComponent(tenantKey)}/users` + ); + return response.users; + } + + /** + * Add a user to a tenant + * @param tenantKey - The unique tenant key + * @param userKey - The user key to add + */ + async addUser(tenantKey: string, userKey: string): Promise { + return this.httpPost(`/tenants/${encodeURIComponent(tenantKey)}/users`, { + user: userKey, + }); + } + + /** + * Remove a user from a tenant + * @param tenantKey - The unique tenant key + * @param userKey - The user key to remove + */ + async removeUser(tenantKey: string, userKey: string): Promise { + return this.httpDelete( + `/tenants/${encodeURIComponent(tenantKey)}/users/${encodeURIComponent( + userKey + )}` + ); + } +} diff --git a/src/api/users.ts b/src/api/users.ts index b77882f..bccc35d 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -1,126 +1,126 @@ -import { BaseApiClient } from "./base"; -import { - IUserCreate, - IUserUpdate, - IUserRead, - IUserList, - IListParams, -} from "../types"; - -/** - * Users API client for managing users in the authorization system - */ -export class UsersApi extends BaseApiClient { - /** - * List all users - * @param params - Pagination and filtering parameters - */ - async list(params?: IListParams): Promise { - return this.httpGet("/users", params as Record); - } - - /** - * Get a user by key - * @param userKey - The unique user key - */ - async get(userKey: string): Promise { - return this.httpGet(`/users/${encodeURIComponent(userKey)}`); - } - - /** - * Create a new user - * @param userData - User creation data - */ - async create(userData: IUserCreate): Promise { - return this.httpPost("/users", userData); - } - - /** - * Update an existing user - * @param userKey - The unique user key - * @param userData - User update data - */ - async update(userKey: string, userData: IUserUpdate): Promise { - return this.httpPatch( - `/users/${encodeURIComponent(userKey)}`, - userData - ); - } - - /** - * Delete a user - * @param userKey - The unique user key - */ - async delete(userKey: string): Promise { - return this.httpDelete(`/users/${encodeURIComponent(userKey)}`); - } - - /** - * Sync a user (create or update) - * @param userData - User data to sync - */ - async sync(userData: IUserCreate): Promise { - return this.httpPut( - `/users/${encodeURIComponent(userData.key)}`, - userData - ); - } - - /** - * Get user's assigned roles - * @param userKey - The unique user key - */ - async getRoles(userKey: string): Promise { - const response = await this.httpGet<{ roles: string[] }>( - `/users/${encodeURIComponent(userKey)}/roles` - ); - return response.roles; - } - - /** - * Assign a role to a user - * @param userKey - The unique user key - * @param roleKey - The role key to assign - * @param tenant - Optional tenant key - */ - async assignRole( - userKey: string, - roleKey: string, - tenant?: string - ): Promise { - return this.httpPost(`/users/${encodeURIComponent(userKey)}/roles`, { - role: roleKey, - tenant, - }); - } - - /** - * Unassign a role from a user - * @param userKey - The unique user key - * @param roleKey - The role key to unassign - * @param tenant - Optional tenant key - */ - async unassignRole( - userKey: string, - roleKey: string, - tenant?: string - ): Promise { - const params = tenant ? `?tenant=${encodeURIComponent(tenant)}` : ""; - return this.httpDelete( - `/users/${encodeURIComponent(userKey)}/roles/${encodeURIComponent( - roleKey - )}${params}` - ); - } - - /** - * Get user's tenants - * @param userKey - The unique user key - */ - async getTenants(userKey: string): Promise { - const response = await this.httpGet<{ tenants: string[] }>( - `/users/${encodeURIComponent(userKey)}/tenants` - ); - return response.tenants; - } -} +import { BaseApiClient } from "./base"; +import { + IUserCreate, + IUserUpdate, + IUserRead, + IUserList, + IListParams, +} from "../types"; + +/** + * Users API client for managing users in the authorization system + */ +export class UsersApi extends BaseApiClient { + /** + * List all users + * @param params - Pagination and filtering parameters + */ + async list(params?: IListParams): Promise { + return this.httpGet("/users", params as Record); + } + + /** + * Get a user by key + * @param userKey - The unique user key + */ + async get(userKey: string): Promise { + return this.httpGet(`/users/${encodeURIComponent(userKey)}`); + } + + /** + * Create a new user + * @param userData - User creation data + */ + async create(userData: IUserCreate): Promise { + return this.httpPost("/users", userData); + } + + /** + * Update an existing user + * @param userKey - The unique user key + * @param userData - User update data + */ + async update(userKey: string, userData: IUserUpdate): Promise { + return this.httpPatch( + `/users/${encodeURIComponent(userKey)}`, + userData + ); + } + + /** + * Delete a user + * @param userKey - The unique user key + */ + async delete(userKey: string): Promise { + return this.httpDelete(`/users/${encodeURIComponent(userKey)}`); + } + + /** + * Sync a user (create or update) + * @param userData - User data to sync + */ + async sync(userData: IUserCreate): Promise { + return this.httpPut( + `/users/${encodeURIComponent(userData.key)}`, + userData + ); + } + + /** + * Get user's assigned roles + * @param userKey - The unique user key + */ + async getRoles(userKey: string): Promise { + const response = await this.httpGet<{ roles: string[] }>( + `/users/${encodeURIComponent(userKey)}/roles` + ); + return response.roles; + } + + /** + * Assign a role to a user + * @param userKey - The unique user key + * @param roleKey - The role key to assign + * @param tenant - Optional tenant key + */ + async assignRole( + userKey: string, + roleKey: string, + tenant?: string + ): Promise { + return this.httpPost(`/users/${encodeURIComponent(userKey)}/roles`, { + role: roleKey, + tenant, + }); + } + + /** + * Unassign a role from a user + * @param userKey - The unique user key + * @param roleKey - The role key to unassign + * @param tenant - Optional tenant key + */ + async unassignRole( + userKey: string, + roleKey: string, + tenant?: string + ): Promise { + const params = tenant ? `?tenant=${encodeURIComponent(tenant)}` : ""; + return this.httpDelete( + `/users/${encodeURIComponent(userKey)}/roles/${encodeURIComponent( + roleKey + )}${params}` + ); + } + + /** + * Get user's tenants + * @param userKey - The unique user key + */ + async getTenants(userKey: string): Promise { + const response = await this.httpGet<{ tenants: string[] }>( + `/users/${encodeURIComponent(userKey)}/tenants` + ); + return response.tenants; + } +} diff --git a/src/config/index.ts b/src/config/index.ts index 0cccc8f..3ec9a7f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,155 +1,155 @@ -/** - * Permis.io SDK Configuration - */ -export interface IPermisConfig { - /** - * API key for authentication (required) - * Format: permis_key_<64-char-hex> - */ - token: string; - - /** - * Base URL for the Permis.io API - * @default "https://api.permis.io" - */ - apiUrl?: string; - - /** - * Project ID (optional - will be auto-fetched from API key scope if not provided) - */ - projectId?: string; - - /** - * Environment ID (optional - will be auto-fetched from API key scope if not provided) - */ - environmentId?: string; - - /** - * Enable debug logging - * @default false - */ - debug?: boolean; - - /** - * Request timeout in milliseconds - * @default 30000 - */ - timeout?: number; - - /** - * Custom headers to include in all requests - */ - customHeaders?: Record; - - /** - * Number of retry attempts for failed requests - * @default 3 - */ - retryAttempts?: number; - - /** - * Enable throwing exceptions on errors - * @default true - */ - throwOnError?: boolean; -} - -/** - * Internal resolved configuration with all defaults applied - */ -export interface IResolvedConfig { - token: string; - apiUrl: string; - projectId?: string; - environmentId?: string; - debug: boolean; - timeout: number; - customHeaders: Record; - retryAttempts: number; - throwOnError: boolean; -} - -/** - * Mutable configuration that can be updated after scope fetch - */ -export class MutableConfig implements IResolvedConfig { - token: string; - apiUrl: string; - projectId?: string; - environmentId?: string; - debug: boolean; - timeout: number; - customHeaders: Record; - retryAttempts: number; - throwOnError: boolean; - - constructor(config: IResolvedConfig) { - this.token = config.token; - this.apiUrl = config.apiUrl; - this.projectId = config.projectId; - this.environmentId = config.environmentId; - this.debug = config.debug; - this.timeout = config.timeout; - this.customHeaders = config.customHeaders; - this.retryAttempts = config.retryAttempts; - this.throwOnError = config.throwOnError; - } - - /** - * Update project and environment IDs from API key scope - */ - updateScope(projectId?: string, environmentId?: string): void { - if (projectId && !this.projectId) { - this.projectId = projectId; - } - if (environmentId && !this.environmentId) { - this.environmentId = environmentId; - } - } - - /** - * Check if scope (project and environment) is configured - */ - hasScope(): boolean { - return !!(this.projectId && this.environmentId); - } -} - -/** - * Default configuration values - */ -export const DEFAULT_CONFIG: Omit = { - apiUrl: "https://api.permis.io", - debug: false, - timeout: 30000, - customHeaders: {}, - retryAttempts: 3, - throwOnError: true, -}; - -/** - * Resolve configuration by applying defaults - */ -export function resolveConfig(config: IPermisConfig): MutableConfig { - if (!config.token) { - throw new Error("Permis.io SDK: API token is required"); - } - - if (!config.token.startsWith("permis_key_")) { - throw new Error( - "Permis.io SDK: Invalid API key format. Expected format: permis_key_" - ); - } - - return new MutableConfig({ - token: config.token, - apiUrl: config.apiUrl ?? DEFAULT_CONFIG.apiUrl, - projectId: config.projectId, - environmentId: config.environmentId, - debug: config.debug ?? DEFAULT_CONFIG.debug, - timeout: config.timeout ?? DEFAULT_CONFIG.timeout, - customHeaders: { ...DEFAULT_CONFIG.customHeaders, ...config.customHeaders }, - retryAttempts: config.retryAttempts ?? DEFAULT_CONFIG.retryAttempts, - throwOnError: config.throwOnError ?? DEFAULT_CONFIG.throwOnError, - }); -} +/** + * Permissio.io SDK Configuration + */ +export interface IPermissioConfig { + /** + * API key for authentication (required) + * Format: permis_key_<64-char-hex> + */ + token: string; + + /** + * Base URL for the Permissio.io API + * @default "https://api.permissio.io" + */ + apiUrl?: string; + + /** + * Project ID (optional - will be auto-fetched from API key scope if not provided) + */ + projectId?: string; + + /** + * Environment ID (optional - will be auto-fetched from API key scope if not provided) + */ + environmentId?: string; + + /** + * Enable debug logging + * @default false + */ + debug?: boolean; + + /** + * Request timeout in milliseconds + * @default 30000 + */ + timeout?: number; + + /** + * Custom headers to include in all requests + */ + customHeaders?: Record; + + /** + * Number of retry attempts for failed requests + * @default 3 + */ + retryAttempts?: number; + + /** + * Enable throwing exceptions on errors + * @default true + */ + throwOnError?: boolean; +} + +/** + * Internal resolved configuration with all defaults applied + */ +export interface IResolvedConfig { + token: string; + apiUrl: string; + projectId?: string; + environmentId?: string; + debug: boolean; + timeout: number; + customHeaders: Record; + retryAttempts: number; + throwOnError: boolean; +} + +/** + * Mutable configuration that can be updated after scope fetch + */ +export class MutableConfig implements IResolvedConfig { + token: string; + apiUrl: string; + projectId?: string; + environmentId?: string; + debug: boolean; + timeout: number; + customHeaders: Record; + retryAttempts: number; + throwOnError: boolean; + + constructor(config: IResolvedConfig) { + this.token = config.token; + this.apiUrl = config.apiUrl; + this.projectId = config.projectId; + this.environmentId = config.environmentId; + this.debug = config.debug; + this.timeout = config.timeout; + this.customHeaders = config.customHeaders; + this.retryAttempts = config.retryAttempts; + this.throwOnError = config.throwOnError; + } + + /** + * Update project and environment IDs from API key scope + */ + updateScope(projectId?: string, environmentId?: string): void { + if (projectId && !this.projectId) { + this.projectId = projectId; + } + if (environmentId && !this.environmentId) { + this.environmentId = environmentId; + } + } + + /** + * Check if scope (project and environment) is configured + */ + hasScope(): boolean { + return !!(this.projectId && this.environmentId); + } +} + +/** + * Default configuration values + */ +export const DEFAULT_CONFIG: Omit = { + apiUrl: "https://api.permissio.io", + debug: false, + timeout: 30000, + customHeaders: {}, + retryAttempts: 3, + throwOnError: true, +}; + +/** + * Resolve configuration by applying defaults + */ +export function resolveConfig(config: IPermissioConfig): MutableConfig { + if (!config.token) { + throw new Error("Permissio.io SDK: API token is required"); + } + + if (!config.token.startsWith("permis_key_")) { + throw new Error( + "Permissio.io SDK: Invalid API key format. Expected format: permis_key_" + ); + } + + return new MutableConfig({ + token: config.token, + apiUrl: config.apiUrl ?? DEFAULT_CONFIG.apiUrl, + projectId: config.projectId, + environmentId: config.environmentId, + debug: config.debug ?? DEFAULT_CONFIG.debug, + timeout: config.timeout ?? DEFAULT_CONFIG.timeout, + customHeaders: { ...DEFAULT_CONFIG.customHeaders, ...config.customHeaders }, + retryAttempts: config.retryAttempts ?? DEFAULT_CONFIG.retryAttempts, + throwOnError: config.throwOnError ?? DEFAULT_CONFIG.throwOnError, + }); +} diff --git a/src/index.ts b/src/index.ts index b625576..dc33978 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,112 +1,112 @@ -/** - * Permis.io Node.js SDK - * - * @packageDocumentation - * - * @example - * ```typescript - * import { Permis } from 'permisio'; - * - * // Simplest usage - project and environment are auto-detected from API key - * const permis = new Permis({ - * token: 'permis_key_your_api_key_here', - * }); - * - * // Or with explicit project and environment IDs - * const permis2 = new Permis({ - * token: 'permis_key_your_api_key_here', - * projectId: 'your-project-id', - * environmentId: 'your-environment-id', - * }); - * - * // Check permissions - * const allowed = await permis.check({ - * user: 'user@example.com', - * action: 'read', - * resource: 'document', - * }); - * - * if (allowed) { - * console.log('Access granted!'); - * } else { - * console.log('Access denied!'); - * } - * ``` - */ - -// Main SDK class -export { Permis, createPermis } from "./permis"; -export type { IPermisApi } from "./permis"; - -// Configuration -export { resolveConfig, DEFAULT_CONFIG, MutableConfig } from "./config"; -export type { IPermisConfig, IResolvedConfig } from "./config"; - -// API clients -export { - BaseApiClient, - PermisApiError, - UsersApi, - TenantsApi, - RolesApi, - ResourcesApi, - RoleAssignmentsApi, -} from "./api"; -export type { ILogger } from "./api"; - -// Types -export type { - // User types - IUser, - IUserCreate, - IUserUpdate, - IUserRead, - IUserList, - // Tenant types - ITenant, - ITenantCreate, - ITenantUpdate, - ITenantRead, - ITenantList, - // Role types - IRole, - IRoleCreate, - IRoleUpdate, - IRoleRead, - IRoleList, - // Resource types - IResource, - IResourceCreate, - IResourceUpdate, - IResourceRead, - IResourceList, - IResourceInstance, - IResourceInstanceCreate, - IResourceInstanceRead, - // Role assignment types - IRoleAssignment, - IRoleAssignmentCreate, - IRoleAssignmentRead, - IRoleAssignmentRemove, - IRoleAssignmentList, - IBulkRoleAssignment, - IBulkRoleAssignmentResponse, - // Check types - ICheckRequest, - ICheckUser, - ICheckResource, - ICheckResponse, - IBulkCheckRequest, - IBulkCheckResponse, - IGetPermissionsRequest, - IGetPermissionsResponse, - // Common types - IPaginationParams, - IListParams, - IApiError, - IApiResponse, - IApiKeyScope, -} from "./types"; - -// Default export -export { Permis as default } from "./permis"; +/** + * Permissio.io Node.js SDK + * + * @packageDocumentation + * + * @example + * ```typescript + * import { Permissio } from 'permissio'; + * + * // Simplest usage - project and environment are auto-detected from API key + * const permissio = new Permissio({ + * token: 'permis_key_your_api_key_here', + * }); + * + * // Or with explicit project and environment IDs + * const permissio2 = new Permissio({ + * token: 'permis_key_your_api_key_here', + * projectId: 'your-project-id', + * environmentId: 'your-environment-id', + * }); + * + * // Check permissions + * const allowed = await permis.check({ + * user: 'user@example.com', + * action: 'read', + * resource: 'document', + * }); + * + * if (allowed) { + * console.log('Access granted!'); + * } else { + * console.log('Access denied!'); + * } + * ``` + */ + +// Main SDK class +export { Permissio, createPermissio } from "./permissio"; +export type { IPermissioApi } from "./permissio"; + +// Configuration +export { resolveConfig, DEFAULT_CONFIG, MutableConfig } from "./config"; +export type { IPermissioConfig, IResolvedConfig } from "./config"; + +// API clients +export { + BaseApiClient, + PermissioApiError, + UsersApi, + TenantsApi, + RolesApi, + ResourcesApi, + RoleAssignmentsApi, +} from "./api"; +export type { ILogger } from "./api"; + +// Types +export type { + // User types + IUser, + IUserCreate, + IUserUpdate, + IUserRead, + IUserList, + // Tenant types + ITenant, + ITenantCreate, + ITenantUpdate, + ITenantRead, + ITenantList, + // Role types + IRole, + IRoleCreate, + IRoleUpdate, + IRoleRead, + IRoleList, + // Resource types + IResource, + IResourceCreate, + IResourceUpdate, + IResourceRead, + IResourceList, + IResourceInstance, + IResourceInstanceCreate, + IResourceInstanceRead, + // Role assignment types + IRoleAssignment, + IRoleAssignmentCreate, + IRoleAssignmentRead, + IRoleAssignmentRemove, + IRoleAssignmentList, + IBulkRoleAssignment, + IBulkRoleAssignmentResponse, + // Check types + ICheckRequest, + ICheckUser, + ICheckResource, + ICheckResponse, + IBulkCheckRequest, + IBulkCheckResponse, + IGetPermissionsRequest, + IGetPermissionsResponse, + // Common types + IPaginationParams, + IListParams, + IApiError, + IApiResponse, + IApiKeyScope, +} from "./types"; + +// Default export +export { Permissio as default } from "./permissio"; diff --git a/src/permis.ts b/src/permissio.ts similarity index 90% rename from src/permis.ts rename to src/permissio.ts index 61c648b..9814bcf 100644 --- a/src/permis.ts +++ b/src/permissio.ts @@ -1,666 +1,666 @@ -import { IPermisConfig, MutableConfig, resolveConfig } from "./config"; -import { - UsersApi, - TenantsApi, - RolesApi, - ResourcesApi, - RoleAssignmentsApi, - PermisApiError, -} from "./api"; -import { - ICheckRequest, - ICheckResponse, - ICheckUser, - ICheckResource, - IBulkCheckRequest, - IBulkCheckResponse, - IGetPermissionsRequest, - IGetPermissionsResponse, - IRoleRead, - IApiKeyScope, -} from "./types"; -import axios from "axios"; - -/** - * API object containing all API clients - */ -export interface IPermisApi { - users: UsersApi; - tenants: TenantsApi; - roles: RolesApi; - resources: ResourcesApi; - roleAssignments: RoleAssignmentsApi; -} - -/** - * Main Permis.io SDK client - * - * @example - * ```typescript - * import { Permis } from 'permisio'; - * - * const permis = new Permis({ - * token: 'permis_key_your_api_key_here', - * projectId: 'your-project-id', - * environmentId: 'your-environment-id', - * }); - * - * // Check permissions - * const allowed = await permis.check({ - * user: 'user@example.com', - * action: 'read', - * resource: 'document', - * }); - * - * // Manage users - * await permis.api.users.create({ - * key: 'user@example.com', - * email: 'user@example.com', - * firstName: 'John', - * lastName: 'Doe', - * }); - * - * // Assign roles - * await permis.api.roleAssignments.assign({ - * user: 'user@example.com', - * role: 'admin', - * tenant: 'acme-corp', - * }); - * ``` - */ -export class Permis { - /** - * Mutable configuration that can be updated with scope - */ - private readonly config: MutableConfig; - - /** - * API clients for managing resources - */ - public readonly api: IPermisApi; - - /** - * Flag to track if scope has been initialized - */ - private scopeInitialized: boolean = false; - - /** - * Promise for scope initialization (for deduplication) - */ - private scopeInitPromise: Promise | null = null; - - /** - * Create a new Permis SDK instance. - * If projectId and environmentId are not provided, they will be - * auto-fetched from the API key scope on first API call. - * - * @param config - SDK configuration - */ - constructor(config: IPermisConfig) { - this.config = resolveConfig(config); - - // Initialize API clients - const users = new UsersApi(this.config); - const tenants = new TenantsApi(this.config); - const roles = new RolesApi(this.config); - const resources = new ResourcesApi(this.config); - const roleAssignments = new RoleAssignmentsApi(this.config); - - this.api = { - users, - tenants, - roles, - resources, - roleAssignments, - }; - } - - /** - * Check if a user has permission to perform an action on a resource - * - * @example - * ```typescript - * // Simple check - * const allowed = await permis.check({ - * user: 'user@example.com', - * action: 'read', - * resource: 'document', - * }); - * - * // Check with resource instance - * const allowed = await permis.check({ - * user: 'user@example.com', - * action: 'edit', - * resource: { type: 'document', key: 'doc-123' }, - * }); - * - * // Check with tenant context - * const allowed = await permis.check({ - * user: 'user@example.com', - * action: 'delete', - * resource: 'document', - * tenant: 'acme-corp', - * }); - * ``` - */ - async check(request: ICheckRequest): Promise { - const response = await this.checkWithDetails(request); - return response.allowed; - } - - /** - * Check permission with full response details. - * This performs client-side permission checking by: - * 1. Fetching user's role assignments - * 2. Fetching role definitions with permissions - * 3. Checking if any role grants the required permission - */ - async checkWithDetails(request: ICheckRequest): Promise { - // Ensure scope is initialized before making API calls - await this.ensureScope(); - - try { - const userKey = this.getUserKey(request.user); - const resourceType = this.extractResourceType(request.resource); - const resourceInstanceKey = this.extractResourceInstanceKey( - request.resource - ); - const requiredPermission = `${resourceType}:${request.action}`; - - if (this.config.debug) { - console.log( - `[Permis Debug] Check: user=${userKey}, action=${request.action}, resource=${request.resource}` - ); - console.log( - `[Permis Debug] Required permission: ${requiredPermission}` - ); - } - - // 1. Get user's role assignments (filtered by tenant if provided) - const assignmentsResponse = await this.api.roleAssignments.list({ - user: userKey, - tenant: request.tenant, - }); - - if (this.config.debug) { - console.log( - `[Permis Debug] Role assignments:`, - JSON.stringify(assignmentsResponse.data, null, 2) - ); - } - - if (!assignmentsResponse.data || assignmentsResponse.data.length === 0) { - return { - allowed: false, - reason: `User ${userKey} has no role assignments`, - }; - } - - // 2. Get unique role keys from assignments - const roleKeys = [ - ...new Set(assignmentsResponse.data.map((a) => a.role)), - ]; - - if (this.config.debug) { - console.log(`[Permis Debug] User's role keys:`, roleKeys); - } - - // 3. Fetch all roles and build permission map (with role inheritance) - const rolesResponse = await this.api.roles.list({ perPage: 100 }); - const rolesMap = new Map(); - for (const role of rolesResponse.data || []) { - rolesMap.set(role.key, role); - } - - if (this.config.debug) { - console.log( - `[Permis Debug] Available roles:`, - Array.from(rolesMap.keys()) - ); - for (const [key, role] of rolesMap) { - console.log( - `[Permis Debug] Role "${key}" permissions:`, - role.permissions - ); - } - } - - // 4. Check if any assigned role grants the required permission - const matchedRoles: string[] = []; - const matchedPermissions: string[] = []; - - for (const roleKey of roleKeys) { - const permissions = this.getRolePermissions( - roleKey, - rolesMap, - new Set() - ); - if (this.config.debug) { - console.log( - `[Permis Debug] Role "${roleKey}" has permissions:`, - permissions - ); - } - if ( - permissions.includes(requiredPermission) || - permissions.includes(`${resourceType}:*`) || - permissions.includes("*:*") - ) { - matchedRoles.push(roleKey); - matchedPermissions.push(requiredPermission); - } - } - - const allowed = matchedRoles.length > 0; - - if (this.config.debug) { - console.log( - `[Permis Debug] Result: allowed=${allowed}, matchedRoles=`, - matchedRoles - ); - } - - return { - allowed, - reason: allowed - ? `Granted by role(s): ${matchedRoles.join(", ")}` - : `No role grants permission ${requiredPermission}`, - debug: { - matchedRoles, - matchedPermissions, - evaluationTime: 0, - }, - }; - } catch (error) { - if (this.config.throwOnError) { - throw error; - } - return { - allowed: false, - reason: `Error during permission check: ${error}`, - }; - } - } - - /** - * Get all permissions for a role, including inherited permissions - */ - private getRolePermissions( - roleKey: string, - rolesMap: Map, - visited: Set - ): string[] { - // Prevent circular inheritance - if (visited.has(roleKey)) { - return []; - } - visited.add(roleKey); - - const role = rolesMap.get(roleKey); - if (!role) { - return []; - } - - const permissions: string[] = [...(role.permissions || [])]; - - // Add inherited permissions from parent roles - if (role.extends && role.extends.length > 0) { - for (const parentRoleKey of role.extends) { - const parentPermissions = this.getRolePermissions( - parentRoleKey, - rolesMap, - visited - ); - permissions.push(...parentPermissions); - } - } - - return [...new Set(permissions)]; // Remove duplicates - } - - /** - * Perform bulk permission checks - * - * @example - * ```typescript - * const results = await permis.bulkCheck({ - * checks: [ - * { user: 'user1@example.com', action: 'read', resource: 'document' }, - * { user: 'user1@example.com', action: 'write', resource: 'document' }, - * { user: 'user2@example.com', action: 'read', resource: 'document' }, - * ], - * }); - * ``` - */ - async bulkCheck(request: IBulkCheckRequest): Promise { - try { - const results = await Promise.all( - request.checks.map(async (check) => ({ - request: check, - response: await this.checkWithDetails(check), - })) - ); - return { results }; - } catch (error) { - if (this.config.throwOnError) { - throw error; - } - return { - results: request.checks.map((check) => ({ - request: check, - response: { allowed: false, reason: "Error during permission check" }, - })), - }; - } - } - - /** - * Get all permissions for a user - * - * @example - * ```typescript - * const permissions = await permis.getPermissions({ - * user: 'user@example.com', - * tenant: 'acme-corp', - * }); - * - * console.log(permissions.roles); // ['admin', 'editor'] - * console.log(permissions.permissions); // ['document:read', 'document:write'] - * ``` - */ - async getPermissions( - request: IGetPermissionsRequest - ): Promise { - // Ensure scope is initialized before making API calls - await this.ensureScope(); - - try { - // 1. Get user's role assignments - const assignmentsResponse = await this.api.roleAssignments.list({ - user: request.user, - tenant: request.tenant, - resource: request.resource, - }); - - if (!assignmentsResponse.data || assignmentsResponse.data.length === 0) { - return { roles: [], permissions: [] }; - } - - // 2. Get unique role keys - const roleKeys = [ - ...new Set(assignmentsResponse.data.map((a) => a.role)), - ]; - - // 3. Fetch all roles - const rolesResponse = await this.api.roles.list({ perPage: 100 }); - const rolesMap = new Map(); - for (const role of rolesResponse.data || []) { - rolesMap.set(role.key, role); - } - - // 4. Collect all permissions from assigned roles - const allPermissions: string[] = []; - for (const roleKey of roleKeys) { - const permissions = this.getRolePermissions( - roleKey, - rolesMap, - new Set() - ); - allPermissions.push(...permissions); - } - - return { - roles: roleKeys, - permissions: [...new Set(allPermissions)], - }; - } catch (error) { - if (this.config.throwOnError) { - throw error; - } - return { permissions: [], roles: [] }; - } - } - - /** - * Sync user and optionally assign roles in one call - * - * @example - * ```typescript - * await permis.syncUser({ - * key: 'user@example.com', - * email: 'user@example.com', - * firstName: 'John', - * lastName: 'Doe', - * }); - * ``` - */ - async syncUser(userData: { - key: string; - email?: string; - firstName?: string; - lastName?: string; - attributes?: Record; - roles?: Array<{ role: string; tenant?: string }>; - }) { - const { roles, ...userFields } = userData; - - // Sync user - const user = await this.api.users.sync(userFields); - - // Assign roles if provided - if (roles && roles.length > 0) { - await Promise.all( - roles.map((r) => - this.api.roleAssignments.assign({ - user: userData.key, - role: r.role, - tenant: r.tenant, - }) - ) - ); - } - - return user; - } - - /** - * Convenience method: Check and throw if not allowed - * - * @throws {PermisApiError} If the user is not allowed - */ - async checkAndThrow(request: ICheckRequest): Promise { - const response = await this.checkWithDetails(request); - - if (!response.allowed) { - throw new PermisApiError({ - message: `Access denied: User ${this.getUserKey( - request.user - )} is not allowed to perform ${request.action} on ${this.getResourceKey( - request.resource - )}`, - code: "ACCESS_DENIED", - statusCode: 403, - details: { request, response }, - }); - } - } - - /** - * Get the current configuration - */ - getConfig(): { - token: string; - apiUrl: string; - projectId?: string; - environmentId?: string; - debug: boolean; - timeout: number; - } { - return { - token: this.config.token, - apiUrl: this.config.apiUrl, - projectId: this.config.projectId, - environmentId: this.config.environmentId, - debug: this.config.debug, - timeout: this.config.timeout, - }; - } - - /** - * Get the project and environment IDs (fetches from API key scope if needed) - */ - async getScope(): Promise<{ projectId?: string; environmentId?: string }> { - await this.ensureScope(); - return { - projectId: this.config.projectId, - environmentId: this.config.environmentId, - }; - } - - /** - * Ensure scope (projectId and environmentId) is available. - * Fetches from API key scope endpoint if not already configured. - */ - private async ensureScope(): Promise { - // Already initialized or already has scope from config - if (this.scopeInitialized || this.config.hasScope()) { - return; - } - - // Deduplicate concurrent initialization calls - if (this.scopeInitPromise) { - return this.scopeInitPromise; - } - - this.scopeInitPromise = this.fetchAndSetScope(); - try { - await this.scopeInitPromise; - } finally { - this.scopeInitPromise = null; - } - } - - /** - * Fetch scope from API key scope endpoint and update config - */ - private async fetchAndSetScope(): Promise { - try { - const response = await axios.get( - `${this.config.apiUrl}/v1/api-key/scope`, - { - headers: { - Authorization: `Bearer ${this.config.token}`, - "Content-Type": "application/json", - }, - timeout: this.config.timeout, - } - ); - - const scope = response.data; - this.config.updateScope(scope.project_id, scope.environment_id); - this.scopeInitialized = true; - - if (this.config.debug) { - console.debug("[Permis.io SDK] Auto-fetched scope:", { - projectId: scope.project_id, - environmentId: scope.environment_id, - }); - } - } catch (error) { - // If scope fetch fails and we don't have scope, throw an error - if (!this.config.hasScope()) { - throw new Error( - "Permis.io SDK: Failed to fetch API key scope. " + - "Either provide projectId and environmentId in config, " + - "or ensure the API key has valid scope. " + - `Error: ${error}` - ); - } - // If we already have scope from config, just mark as initialized - this.scopeInitialized = true; - } - } - - // ==================== Private Helpers ==================== - - private buildBaseUrl(): string { - const { projectId, environmentId } = this.config; - - if (projectId && environmentId) { - return `${this.config.apiUrl}/v1/facts/${projectId}/${environmentId}`; - } - - return `${this.config.apiUrl}/v1`; - } - - private buildCheckUrl(): string { - return `${this.buildBaseUrl()}/check`; - } - - private normalizeCheckRequest( - request: ICheckRequest - ): Record { - const user = this.normalizeUser(request.user); - const resource = this.normalizeResource(request.resource); - - return { - user, - action: request.action, - resource, - tenant: request.tenant, - context: request.context, - }; - } - - private normalizeUser(user: string | ICheckUser): Record { - if (typeof user === "string") { - return { key: user }; - } - return user as unknown as Record; - } - - private normalizeResource( - resource: string | ICheckResource - ): Record { - if (typeof resource === "string") { - return { type: resource }; - } - return resource as unknown as Record; - } - - private getUserKey(user: string | ICheckUser): string { - return typeof user === "string" ? user : user.key; - } - - private getResourceKey(resource: string | ICheckResource): string { - if (typeof resource === "string") { - return resource; - } - return resource.key ? `${resource.type}:${resource.key}` : resource.type; - } - - private extractResourceType(resource: string | ICheckResource): string { - if (typeof resource === "string") { - // If string contains ':', the type is before the colon - // Otherwise, the string is the type - return resource.includes(":") ? resource.split(":")[0] : resource; - } - return resource.type; - } - - private extractResourceInstanceKey( - resource: string | ICheckResource - ): string | undefined { - if (typeof resource === "string") { - // If string contains ':', the key is after the colon - const parts = resource.split(":"); - return parts.length > 1 ? parts[1] : undefined; - } - return resource.key; - } -} - -// Export a factory function for convenience -export function createPermis(config: IPermisConfig): Permis { - return new Permis(config); -} +import { IPermissioConfig, MutableConfig, resolveConfig } from "./config"; +import { + UsersApi, + TenantsApi, + RolesApi, + ResourcesApi, + RoleAssignmentsApi, + PermissioApiError, +} from "./api"; +import { + ICheckRequest, + ICheckResponse, + ICheckUser, + ICheckResource, + IBulkCheckRequest, + IBulkCheckResponse, + IGetPermissionsRequest, + IGetPermissionsResponse, + IRoleRead, + IApiKeyScope, +} from "./types"; +import axios from "axios"; + +/** + * API object containing all API clients + */ +export interface IPermissioApi { + users: UsersApi; + tenants: TenantsApi; + roles: RolesApi; + resources: ResourcesApi; + roleAssignments: RoleAssignmentsApi; +} + +/** + * Main Permissio.io SDK client + * + * @example + * ```typescript + * import { Permissio } from 'permissio'; + * + * const permissio = new Permissiosio({ + * token: 'permis_key_your_api_key_here', + * projectId: 'your-project-id', + * environmentId: 'your-environment-id', + * }); + * + * // Check permissions + * const allowed = await permis.check({ + * user: 'user@example.com', + * action: 'read', + * resource: 'document', + * }); + * + * // Manage users + * await permis.api.users.create({ + * key: 'user@example.com', + * email: 'user@example.com', + * firstName: 'John', + * lastName: 'Doe', + * }); + * + * // Assign roles + * await permis.api.roleAssignments.assign({ + * user: 'user@example.com', + * role: 'admin', + * tenant: 'acme-corp', + * }); + * ``` + */ +export class Permissio { + /** + * Mutable configuration that can be updated with scope + */ + private readonly config: MutableConfig; + + /** + * API clients for managing resources + */ + public readonly api: IPermissioApi; + + /** + * Flag to track if scope has been initialized + */ + private scopeInitialized: boolean = false; + + /** + * Promise for scope initialization (for deduplication) + */ + private scopeInitPromise: Promise | null = null; + + /** + * Create a new Permis SDK instance. + * If projectId and environmentId are not provided, they will be + * auto-fetched from the API key scope on first API call. + * + * @param config - SDK configuration + */ + constructor(config: IPermissioConfig) { + this.config = resolveConfig(config); + + // Initialize API clients + const users = new UsersApi(this.config); + const tenants = new TenantsApi(this.config); + const roles = new RolesApi(this.config); + const resources = new ResourcesApi(this.config); + const roleAssignments = new RoleAssignmentsApi(this.config); + + this.api = { + users, + tenants, + roles, + resources, + roleAssignments, + }; + } + + /** + * Check if a user has permission to perform an action on a resource + * + * @example + * ```typescript + * // Simple check + * const allowed = await permis.check({ + * user: 'user@example.com', + * action: 'read', + * resource: 'document', + * }); + * + * // Check with resource instance + * const allowed = await permis.check({ + * user: 'user@example.com', + * action: 'edit', + * resource: { type: 'document', key: 'doc-123' }, + * }); + * + * // Check with tenant context + * const allowed = await permis.check({ + * user: 'user@example.com', + * action: 'delete', + * resource: 'document', + * tenant: 'acme-corp', + * }); + * ``` + */ + async check(request: ICheckRequest): Promise { + const response = await this.checkWithDetails(request); + return response.allowed; + } + + /** + * Check permission with full response details. + * This performs client-side permission checking by: + * 1. Fetching user's role assignments + * 2. Fetching role definitions with permissions + * 3. Checking if any role grants the required permission + */ + async checkWithDetails(request: ICheckRequest): Promise { + // Ensure scope is initialized before making API calls + await this.ensureScope(); + + try { + const userKey = this.getUserKey(request.user); + const resourceType = this.extractResourceType(request.resource); + const resourceInstanceKey = this.extractResourceInstanceKey( + request.resource + ); + const requiredPermission = `${resourceType}:${request.action}`; + + if (this.config.debug) { + console.log( + `[Permissio Debug] Check: user=${userKey}, action=${request.action}, resource=${request.resource}` + ); + console.log( + `[Permissio Debug] Required permission: ${requiredPermission}` + ); + } + + // 1. Get user's role assignments (filtered by tenant if provided) + const assignmentsResponse = await this.api.roleAssignments.list({ + user: userKey, + tenant: request.tenant, + }); + + if (this.config.debug) { + console.log( + `[Permissio Debug] Role assignments:`, + JSON.stringify(assignmentsResponse.data, null, 2) + ); + } + + if (!assignmentsResponse.data || assignmentsResponse.data.length === 0) { + return { + allowed: false, + reason: `User ${userKey} has no role assignments`, + }; + } + + // 2. Get unique role keys from assignments + const roleKeys = [ + ...new Set(assignmentsResponse.data.map((a) => a.role)), + ]; + + if (this.config.debug) { + console.log(`[Permissio Debug] User's role keys:`, roleKeys); + } + + // 3. Fetch all roles and build permission map (with role inheritance) + const rolesResponse = await this.api.roles.list({ perPage: 100 }); + const rolesMap = new Map(); + for (const role of rolesResponse.data || []) { + rolesMap.set(role.key, role); + } + + if (this.config.debug) { + console.log( + `[Permissio Debug] Available roles:`, + Array.from(rolesMap.keys()) + ); + for (const [key, role] of rolesMap) { + console.log( + `[Permissio Debug] Role "${key}" permissions:`, + role.permissions + ); + } + } + + // 4. Check if any assigned role grants the required permission + const matchedRoles: string[] = []; + const matchedPermissions: string[] = []; + + for (const roleKey of roleKeys) { + const permissions = this.getRolePermissions( + roleKey, + rolesMap, + new Set() + ); + if (this.config.debug) { + console.log( + `[Permissio Debug] Role "${roleKey}" has permissions:`, + permissions + ); + } + if ( + permissions.includes(requiredPermission) || + permissions.includes(`${resourceType}:*`) || + permissions.includes("*:*") + ) { + matchedRoles.push(roleKey); + matchedPermissions.push(requiredPermission); + } + } + + const allowed = matchedRoles.length > 0; + + if (this.config.debug) { + console.log( + `[Permissio Debug] Result: allowed=${allowed}, matchedRoles=`, + matchedRoles + ); + } + + return { + allowed, + reason: allowed + ? `Granted by role(s): ${matchedRoles.join(", ")}` + : `No role grants permission ${requiredPermission}`, + debug: { + matchedRoles, + matchedPermissions, + evaluationTime: 0, + }, + }; + } catch (error) { + if (this.config.throwOnError) { + throw error; + } + return { + allowed: false, + reason: `Error during permission check: ${error}`, + }; + } + } + + /** + * Get all permissions for a role, including inherited permissions + */ + private getRolePermissions( + roleKey: string, + rolesMap: Map, + visited: Set + ): string[] { + // Prevent circular inheritance + if (visited.has(roleKey)) { + return []; + } + visited.add(roleKey); + + const role = rolesMap.get(roleKey); + if (!role) { + return []; + } + + const permissions: string[] = [...(role.permissions || [])]; + + // Add inherited permissions from parent roles + if (role.extends && role.extends.length > 0) { + for (const parentRoleKey of role.extends) { + const parentPermissions = this.getRolePermissions( + parentRoleKey, + rolesMap, + visited + ); + permissions.push(...parentPermissions); + } + } + + return [...new Set(permissions)]; // Remove duplicates + } + + /** + * Perform bulk permission checks + * + * @example + * ```typescript + * const results = await permis.bulkCheck({ + * checks: [ + * { user: 'user1@example.com', action: 'read', resource: 'document' }, + * { user: 'user1@example.com', action: 'write', resource: 'document' }, + * { user: 'user2@example.com', action: 'read', resource: 'document' }, + * ], + * }); + * ``` + */ + async bulkCheck(request: IBulkCheckRequest): Promise { + try { + const results = await Promise.all( + request.checks.map(async (check) => ({ + request: check, + response: await this.checkWithDetails(check), + })) + ); + return { results }; + } catch (error) { + if (this.config.throwOnError) { + throw error; + } + return { + results: request.checks.map((check) => ({ + request: check, + response: { allowed: false, reason: "Error during permission check" }, + })), + }; + } + } + + /** + * Get all permissions for a user + * + * @example + * ```typescript + * const permissions = await permis.getPermissions({ + * user: 'user@example.com', + * tenant: 'acme-corp', + * }); + * + * console.log(permissions.roles); // ['admin', 'editor'] + * console.log(permissions.permissions); // ['document:read', 'document:write'] + * ``` + */ + async getPermissions( + request: IGetPermissionsRequest + ): Promise { + // Ensure scope is initialized before making API calls + await this.ensureScope(); + + try { + // 1. Get user's role assignments + const assignmentsResponse = await this.api.roleAssignments.list({ + user: request.user, + tenant: request.tenant, + resource: request.resource, + }); + + if (!assignmentsResponse.data || assignmentsResponse.data.length === 0) { + return { roles: [], permissions: [] }; + } + + // 2. Get unique role keys + const roleKeys = [ + ...new Set(assignmentsResponse.data.map((a) => a.role)), + ]; + + // 3. Fetch all roles + const rolesResponse = await this.api.roles.list({ perPage: 100 }); + const rolesMap = new Map(); + for (const role of rolesResponse.data || []) { + rolesMap.set(role.key, role); + } + + // 4. Collect all permissions from assigned roles + const allPermissions: string[] = []; + for (const roleKey of roleKeys) { + const permissions = this.getRolePermissions( + roleKey, + rolesMap, + new Set() + ); + allPermissions.push(...permissions); + } + + return { + roles: roleKeys, + permissions: [...new Set(allPermissions)], + }; + } catch (error) { + if (this.config.throwOnError) { + throw error; + } + return { permissions: [], roles: [] }; + } + } + + /** + * Sync user and optionally assign roles in one call + * + * @example + * ```typescript + * await permis.syncUser({ + * key: 'user@example.com', + * email: 'user@example.com', + * firstName: 'John', + * lastName: 'Doe', + * }); + * ``` + */ + async syncUser(userData: { + key: string; + email?: string; + firstName?: string; + lastName?: string; + attributes?: Record; + roles?: Array<{ role: string; tenant?: string }>; + }) { + const { roles, ...userFields } = userData; + + // Sync user + const user = await this.api.users.sync(userFields); + + // Assign roles if provided + if (roles && roles.length > 0) { + await Promise.all( + roles.map((r) => + this.api.roleAssignments.assign({ + user: userData.key, + role: r.role, + tenant: r.tenant, + }) + ) + ); + } + + return user; + } + + /** + * Convenience method: Check and throw if not allowed + * + * @throws {PermissioApiError} If the user is not allowed + */ + async checkAndThrow(request: ICheckRequest): Promise { + const response = await this.checkWithDetails(request); + + if (!response.allowed) { + throw new PermissioApiError({ + message: `Access denied: User ${this.getUserKey( + request.user + )} is not allowed to perform ${request.action} on ${this.getResourceKey( + request.resource + )}`, + code: "ACCESS_DENIED", + statusCode: 403, + details: { request, response }, + }); + } + } + + /** + * Get the current configuration + */ + getConfig(): { + token: string; + apiUrl: string; + projectId?: string; + environmentId?: string; + debug: boolean; + timeout: number; + } { + return { + token: this.config.token, + apiUrl: this.config.apiUrl, + projectId: this.config.projectId, + environmentId: this.config.environmentId, + debug: this.config.debug, + timeout: this.config.timeout, + }; + } + + /** + * Get the project and environment IDs (fetches from API key scope if needed) + */ + async getScope(): Promise<{ projectId?: string; environmentId?: string }> { + await this.ensureScope(); + return { + projectId: this.config.projectId, + environmentId: this.config.environmentId, + }; + } + + /** + * Ensure scope (projectId and environmentId) is available. + * Fetches from API key scope endpoint if not already configured. + */ + private async ensureScope(): Promise { + // Already initialized or already has scope from config + if (this.scopeInitialized || this.config.hasScope()) { + return; + } + + // Deduplicate concurrent initialization calls + if (this.scopeInitPromise) { + return this.scopeInitPromise; + } + + this.scopeInitPromise = this.fetchAndSetScope(); + try { + await this.scopeInitPromise; + } finally { + this.scopeInitPromise = null; + } + } + + /** + * Fetch scope from API key scope endpoint and update config + */ + private async fetchAndSetScope(): Promise { + try { + const response = await axios.get( + `${this.config.apiUrl}/v1/api-key/scope`, + { + headers: { + Authorization: `Bearer ${this.config.token}`, + "Content-Type": "application/json", + }, + timeout: this.config.timeout, + } + ); + + const scope = response.data; + this.config.updateScope(scope.project_id, scope.environment_id); + this.scopeInitialized = true; + + if (this.config.debug) { + console.debug("[Permissio.io SDK] Auto-fetched scope:", { + projectId: scope.project_id, + environmentId: scope.environment_id, + }); + } + } catch (error) { + // If scope fetch fails and we don't have scope, throw an error + if (!this.config.hasScope()) { + throw new Error( + "Permissio.io SDK: Failed to fetch API key scope. " + + "Either provide projectId and environmentId in config, " + + "or ensure the API key has valid scope. " + + `Error: ${error}` + ); + } + // If we already have scope from config, just mark as initialized + this.scopeInitialized = true; + } + } + + // ==================== Private Helpers ==================== + + private buildBaseUrl(): string { + const { projectId, environmentId } = this.config; + + if (projectId && environmentId) { + return `${this.config.apiUrl}/v1/facts/${projectId}/${environmentId}`; + } + + return `${this.config.apiUrl}/v1`; + } + + private buildCheckUrl(): string { + return `${this.buildBaseUrl()}/check`; + } + + private normalizeCheckRequest( + request: ICheckRequest + ): Record { + const user = this.normalizeUser(request.user); + const resource = this.normalizeResource(request.resource); + + return { + user, + action: request.action, + resource, + tenant: request.tenant, + context: request.context, + }; + } + + private normalizeUser(user: string | ICheckUser): Record { + if (typeof user === "string") { + return { key: user }; + } + return user as unknown as Record; + } + + private normalizeResource( + resource: string | ICheckResource + ): Record { + if (typeof resource === "string") { + return { type: resource }; + } + return resource as unknown as Record; + } + + private getUserKey(user: string | ICheckUser): string { + return typeof user === "string" ? user : user.key; + } + + private getResourceKey(resource: string | ICheckResource): string { + if (typeof resource === "string") { + return resource; + } + return resource.key ? `${resource.type}:${resource.key}` : resource.type; + } + + private extractResourceType(resource: string | ICheckResource): string { + if (typeof resource === "string") { + // If string contains ':', the type is before the colon + // Otherwise, the string is the type + return resource.includes(":") ? resource.split(":")[0] : resource; + } + return resource.type; + } + + private extractResourceInstanceKey( + resource: string | ICheckResource + ): string | undefined { + if (typeof resource === "string") { + // If string contains ':', the key is after the colon + const parts = resource.split(":"); + return parts.length > 1 ? parts[1] : undefined; + } + return resource.key; + } +} + +// Export a factory function for convenience +export function createPermissio(config: IPermissioConfig): Permissio { + return new Permissio(config); +} diff --git a/src/types/check.ts b/src/types/check.ts index d100a3d..ab2d3fe 100644 --- a/src/types/check.ts +++ b/src/types/check.ts @@ -1,110 +1,110 @@ -/** - * Permission check request - */ -export interface ICheckRequest { - /** - * The user key to check permissions for - */ - user: string | ICheckUser; - - /** - * The action to check (e.g., "read", "write", "delete") - */ - action: string; - - /** - * The resource to check access to - */ - resource: string | ICheckResource; - - /** - * Optional tenant context - */ - tenant?: string; - - /** - * Additional context for the check - */ - context?: Record; -} - -/** - * User object for permission checks - */ -export interface ICheckUser { - key: string; - attributes?: Record; -} - -/** - * Resource object for permission checks - */ -export interface ICheckResource { - type: string; - key?: string; - tenant?: string; - attributes?: Record; -} - -/** - * Permission check response - */ -export interface ICheckResponse { - /** - * Whether the action is allowed - */ - allowed: boolean; - - /** - * Reason for the decision (optional) - */ - reason?: string; - - /** - * Debug information (when debug mode is enabled) - */ - debug?: { - matchedRoles?: string[]; - matchedPermissions?: string[]; - evaluationTime?: number; - }; -} - -/** - * Bulk permission check request - */ -export interface IBulkCheckRequest { - checks: ICheckRequest[]; -} - -/** - * Bulk permission check response - */ -export interface IBulkCheckResponse { - results: Array<{ - request: ICheckRequest; - response: ICheckResponse; - }>; -} - -/** - * Get all permissions for a user - */ -export interface IGetPermissionsRequest { - user: string; - tenant?: string; - resource?: string; -} - -/** - * User permissions response - */ -export interface IGetPermissionsResponse { - permissions: string[]; - roles: string[]; - resources?: Array<{ - type: string; - key?: string; - permissions: string[]; - }>; -} +/** + * Permission check request + */ +export interface ICheckRequest { + /** + * The user key to check permissions for + */ + user: string | ICheckUser; + + /** + * The action to check (e.g., "read", "write", "delete") + */ + action: string; + + /** + * The resource to check access to + */ + resource: string | ICheckResource; + + /** + * Optional tenant context + */ + tenant?: string; + + /** + * Additional context for the check + */ + context?: Record; +} + +/** + * User object for permission checks + */ +export interface ICheckUser { + key: string; + attributes?: Record; +} + +/** + * Resource object for permission checks + */ +export interface ICheckResource { + type: string; + key?: string; + tenant?: string; + attributes?: Record; +} + +/** + * Permission check response + */ +export interface ICheckResponse { + /** + * Whether the action is allowed + */ + allowed: boolean; + + /** + * Reason for the decision (optional) + */ + reason?: string; + + /** + * Debug information (when debug mode is enabled) + */ + debug?: { + matchedRoles?: string[]; + matchedPermissions?: string[]; + evaluationTime?: number; + }; +} + +/** + * Bulk permission check request + */ +export interface IBulkCheckRequest { + checks: ICheckRequest[]; +} + +/** + * Bulk permission check response + */ +export interface IBulkCheckResponse { + results: Array<{ + request: ICheckRequest; + response: ICheckResponse; + }>; +} + +/** + * Get all permissions for a user + */ +export interface IGetPermissionsRequest { + user: string; + tenant?: string; + resource?: string; +} + +/** + * User permissions response + */ +export interface IGetPermissionsResponse { + permissions: string[]; + roles: string[]; + resources?: Array<{ + type: string; + key?: string; + permissions: string[]; + }>; +} diff --git a/src/types/index.ts b/src/types/index.ts index e0fd284..4a2e4e8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,97 +1,97 @@ -// User types -export type { - IUser, - IUserCreate, - IUserUpdate, - IUserRead, - IUserList, -} from "./user"; - -// Tenant types -export type { - ITenant, - ITenantCreate, - ITenantUpdate, - ITenantRead, - ITenantList, -} from "./tenant"; - -// Role types -export type { - IRole, - IRoleCreate, - IRoleUpdate, - IRoleRead, - IRoleList, -} from "./role"; - -// Resource types -export type { - IResource, - IResourceCreate, - IResourceUpdate, - IResourceRead, - IResourceList, - IResourceInstance, - IResourceInstanceCreate, - IResourceInstanceRead, -} from "./resource"; - -// Role assignment types -export type { - IRoleAssignment, - IRoleAssignmentCreate, - IRoleAssignmentRead, - IRoleAssignmentRemove, - IRoleAssignmentList, - IBulkRoleAssignment, - IBulkRoleAssignmentResponse, -} from "./role-assignment"; - -// Check types -export type { - ICheckRequest, - ICheckUser, - ICheckResource, - ICheckResponse, - IBulkCheckRequest, - IBulkCheckResponse, - IGetPermissionsRequest, - IGetPermissionsResponse, -} from "./check"; - -// Common types -export interface IPaginationParams { - page?: number; - perPage?: number; -} - -export interface IListParams extends IPaginationParams { - search?: string; - sortBy?: string; - sortOrder?: "asc" | "desc"; -} - -export interface IApiError { - message: string; - code?: string; - statusCode: number; - details?: Record; -} - -export interface IApiResponse { - data: T; - meta?: { - requestId?: string; - timestamp?: string; - }; -} - -/** - * API Key scope response - returned by /v1/api-key/scope endpoint - */ -export interface IApiKeyScope { - organization_id: string; - project_id?: string; - environment_id?: string; -} +// User types +export type { + IUser, + IUserCreate, + IUserUpdate, + IUserRead, + IUserList, +} from "./user"; + +// Tenant types +export type { + ITenant, + ITenantCreate, + ITenantUpdate, + ITenantRead, + ITenantList, +} from "./tenant"; + +// Role types +export type { + IRole, + IRoleCreate, + IRoleUpdate, + IRoleRead, + IRoleList, +} from "./role"; + +// Resource types +export type { + IResource, + IResourceCreate, + IResourceUpdate, + IResourceRead, + IResourceList, + IResourceInstance, + IResourceInstanceCreate, + IResourceInstanceRead, +} from "./resource"; + +// Role assignment types +export type { + IRoleAssignment, + IRoleAssignmentCreate, + IRoleAssignmentRead, + IRoleAssignmentRemove, + IRoleAssignmentList, + IBulkRoleAssignment, + IBulkRoleAssignmentResponse, +} from "./role-assignment"; + +// Check types +export type { + ICheckRequest, + ICheckUser, + ICheckResource, + ICheckResponse, + IBulkCheckRequest, + IBulkCheckResponse, + IGetPermissionsRequest, + IGetPermissionsResponse, +} from "./check"; + +// Common types +export interface IPaginationParams { + page?: number; + perPage?: number; +} + +export interface IListParams extends IPaginationParams { + search?: string; + sortBy?: string; + sortOrder?: "asc" | "desc"; +} + +export interface IApiError { + message: string; + code?: string; + statusCode: number; + details?: Record; +} + +export interface IApiResponse { + data: T; + meta?: { + requestId?: string; + timestamp?: string; + }; +} + +/** + * API Key scope response - returned by /v1/api-key/scope endpoint + */ +export interface IApiKeyScope { + organization_id: string; + project_id?: string; + environment_id?: string; +} diff --git a/src/types/resource.ts b/src/types/resource.ts index 85f138e..00c56f3 100644 --- a/src/types/resource.ts +++ b/src/types/resource.ts @@ -1,89 +1,89 @@ -/** - * Resource type definition representing a type of resource in the system - */ -export interface IResource { - key: string; - name?: string; - description?: string; - actions?: string[]; - attributes?: Record; -} - -/** - * Resource type creation payload - */ -export interface IResourceCreate { - key: string; - name?: string; - description?: string; - actions?: string[]; - attributes?: Record; -} - -/** - * Resource type update payload - */ -export interface IResourceUpdate { - name?: string; - description?: string; - actions?: string[]; - attributes?: Record; -} - -/** - * Resource type read response from API - */ -export interface IResourceRead { - id: string; - key: string; - name?: string; - description?: string; - actions?: string[]; - attributes?: Record; - createdAt: string; - updatedAt: string; -} - -/** - * Paginated resource type list response - */ -export interface IResourceList { - data: IResourceRead[]; - page: number; - perPage: number; - total: number; - totalPages: number; -} - -/** - * Resource instance representing a specific resource - */ -export interface IResourceInstance { - key: string; - resourceType: string; - tenant?: string; - attributes?: Record; -} - -/** - * Resource instance creation payload - */ -export interface IResourceInstanceCreate { - key: string; - resourceType: string; - tenant?: string; - attributes?: Record; -} - -/** - * Resource instance read response from API - */ -export interface IResourceInstanceRead { - id: string; - key: string; - resourceType: string; - tenant?: string; - attributes?: Record; - createdAt: string; - updatedAt: string; -} +/** + * Resource type definition representing a type of resource in the system + */ +export interface IResource { + key: string; + name?: string; + description?: string; + actions?: string[]; + attributes?: Record; +} + +/** + * Resource type creation payload + */ +export interface IResourceCreate { + key: string; + name?: string; + description?: string; + actions?: string[]; + attributes?: Record; +} + +/** + * Resource type update payload + */ +export interface IResourceUpdate { + name?: string; + description?: string; + actions?: string[]; + attributes?: Record; +} + +/** + * Resource type read response from API + */ +export interface IResourceRead { + id: string; + key: string; + name?: string; + description?: string; + actions?: string[]; + attributes?: Record; + createdAt: string; + updatedAt: string; +} + +/** + * Paginated resource type list response + */ +export interface IResourceList { + data: IResourceRead[]; + page: number; + perPage: number; + total: number; + totalPages: number; +} + +/** + * Resource instance representing a specific resource + */ +export interface IResourceInstance { + key: string; + resourceType: string; + tenant?: string; + attributes?: Record; +} + +/** + * Resource instance creation payload + */ +export interface IResourceInstanceCreate { + key: string; + resourceType: string; + tenant?: string; + attributes?: Record; +} + +/** + * Resource instance read response from API + */ +export interface IResourceInstanceRead { + id: string; + key: string; + resourceType: string; + tenant?: string; + attributes?: Record; + createdAt: string; + updatedAt: string; +} diff --git a/src/types/role-assignment.ts b/src/types/role-assignment.ts index 9fe95fb..3b0e740 100644 --- a/src/types/role-assignment.ts +++ b/src/types/role-assignment.ts @@ -1,76 +1,76 @@ -/** - * Role assignment model representing a user's role on a resource/tenant - */ -export interface IRoleAssignment { - user: string; - role: string; - tenant?: string; - resource?: string; - resourceInstance?: string; -} - -/** - * Role assignment creation payload - */ -export interface IRoleAssignmentCreate { - user: string; - role: string; - tenant?: string; - resource?: string; - resourceInstance?: string; -} - -/** - * Role assignment read response from API - */ -export interface IRoleAssignmentRead { - id: string; - user: string; - role: string; - tenant?: string; - resource?: string; - resourceInstance?: string; - createdAt: string; - updatedAt: string; -} - -/** - * Role assignment remove payload - */ -export interface IRoleAssignmentRemove { - user: string; - role: string; - tenant?: string; - resource?: string; - resourceInstance?: string; -} - -/** - * Paginated role assignment list response - */ -export interface IRoleAssignmentList { - data: IRoleAssignmentRead[]; - page: number; - perPage: number; - total: number; - totalPages: number; -} - -/** - * Bulk role assignment payload - */ -export interface IBulkRoleAssignment { - assignments: IRoleAssignmentCreate[]; -} - -/** - * Bulk role assignment response - */ -export interface IBulkRoleAssignmentResponse { - created: number; - failed: number; - errors?: Array<{ - assignment: IRoleAssignmentCreate; - error: string; - }>; -} +/** + * Role assignment model representing a user's role on a resource/tenant + */ +export interface IRoleAssignment { + user: string; + role: string; + tenant?: string; + resource?: string; + resourceInstance?: string; +} + +/** + * Role assignment creation payload + */ +export interface IRoleAssignmentCreate { + user: string; + role: string; + tenant?: string; + resource?: string; + resourceInstance?: string; +} + +/** + * Role assignment read response from API + */ +export interface IRoleAssignmentRead { + id: string; + user: string; + role: string; + tenant?: string; + resource?: string; + resourceInstance?: string; + createdAt: string; + updatedAt: string; +} + +/** + * Role assignment remove payload + */ +export interface IRoleAssignmentRemove { + user: string; + role: string; + tenant?: string; + resource?: string; + resourceInstance?: string; +} + +/** + * Paginated role assignment list response + */ +export interface IRoleAssignmentList { + data: IRoleAssignmentRead[]; + page: number; + perPage: number; + total: number; + totalPages: number; +} + +/** + * Bulk role assignment payload + */ +export interface IBulkRoleAssignment { + assignments: IRoleAssignmentCreate[]; +} + +/** + * Bulk role assignment response + */ +export interface IBulkRoleAssignmentResponse { + created: number; + failed: number; + errors?: Array<{ + assignment: IRoleAssignmentCreate; + error: string; + }>; +} diff --git a/src/types/role.ts b/src/types/role.ts index a847aaf..e4574ad 100644 --- a/src/types/role.ts +++ b/src/types/role.ts @@ -1,60 +1,60 @@ -/** - * Role model representing a role that can be assigned to users - */ -export interface IRole { - key: string; - name?: string; - description?: string; - permissions?: string[]; - extends?: string[]; - attributes?: Record; -} - -/** - * Role creation payload - */ -export interface IRoleCreate { - key: string; - name?: string; - description?: string; - permissions?: string[]; - extends?: string[]; - attributes?: Record; -} - -/** - * Role update payload - */ -export interface IRoleUpdate { - name?: string; - description?: string; - permissions?: string[]; - extends?: string[]; - attributes?: Record; -} - -/** - * Role read response from API - */ -export interface IRoleRead { - id: string; - key: string; - name?: string; - description?: string; - permissions?: string[]; - extends?: string[]; - attributes?: Record; - createdAt: string; - updatedAt: string; -} - -/** - * Paginated role list response - */ -export interface IRoleList { - data: IRoleRead[]; - page: number; - perPage: number; - total: number; - totalPages: number; -} +/** + * Role model representing a role that can be assigned to users + */ +export interface IRole { + key: string; + name?: string; + description?: string; + permissions?: string[]; + extends?: string[]; + attributes?: Record; +} + +/** + * Role creation payload + */ +export interface IRoleCreate { + key: string; + name?: string; + description?: string; + permissions?: string[]; + extends?: string[]; + attributes?: Record; +} + +/** + * Role update payload + */ +export interface IRoleUpdate { + name?: string; + description?: string; + permissions?: string[]; + extends?: string[]; + attributes?: Record; +} + +/** + * Role read response from API + */ +export interface IRoleRead { + id: string; + key: string; + name?: string; + description?: string; + permissions?: string[]; + extends?: string[]; + attributes?: Record; + createdAt: string; + updatedAt: string; +} + +/** + * Paginated role list response + */ +export interface IRoleList { + data: IRoleRead[]; + page: number; + perPage: number; + total: number; + totalPages: number; +} diff --git a/src/types/tenant.ts b/src/types/tenant.ts index 0ec38bb..6601b22 100644 --- a/src/types/tenant.ts +++ b/src/types/tenant.ts @@ -1,52 +1,52 @@ -/** - * Tenant model representing a customer/organization in multi-tenant systems - */ -export interface ITenant { - key: string; - name?: string; - description?: string; - attributes?: Record; -} - -/** - * Tenant creation payload - */ -export interface ITenantCreate { - key: string; - name?: string; - description?: string; - attributes?: Record; -} - -/** - * Tenant update payload - */ -export interface ITenantUpdate { - name?: string; - description?: string; - attributes?: Record; -} - -/** - * Tenant read response from API - */ -export interface ITenantRead { - id: string; - key: string; - name?: string; - description?: string; - attributes?: Record; - createdAt: string; - updatedAt: string; -} - -/** - * Paginated tenant list response - */ -export interface ITenantList { - data: ITenantRead[]; - page: number; - perPage: number; - total: number; - totalPages: number; -} +/** + * Tenant model representing a customer/organization in multi-tenant systems + */ +export interface ITenant { + key: string; + name?: string; + description?: string; + attributes?: Record; +} + +/** + * Tenant creation payload + */ +export interface ITenantCreate { + key: string; + name?: string; + description?: string; + attributes?: Record; +} + +/** + * Tenant update payload + */ +export interface ITenantUpdate { + name?: string; + description?: string; + attributes?: Record; +} + +/** + * Tenant read response from API + */ +export interface ITenantRead { + id: string; + key: string; + name?: string; + description?: string; + attributes?: Record; + createdAt: string; + updatedAt: string; +} + +/** + * Paginated tenant list response + */ +export interface ITenantList { + data: ITenantRead[]; + page: number; + perPage: number; + total: number; + totalPages: number; +} diff --git a/src/types/user.ts b/src/types/user.ts index 9b13658..cf92d7e 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,56 +1,56 @@ -/** - * User model representing an end-user in the authorization system - */ -export interface IUser { - key: string; - email?: string; - firstName?: string; - lastName?: string; - attributes?: Record; -} - -/** - * User creation payload - */ -export interface IUserCreate { - key: string; - email?: string; - firstName?: string; - lastName?: string; - attributes?: Record; -} - -/** - * User update payload - */ -export interface IUserUpdate { - email?: string; - firstName?: string; - lastName?: string; - attributes?: Record; -} - -/** - * User read response from API - */ -export interface IUserRead { - id: string; - key: string; - email?: string; - firstName?: string; - lastName?: string; - attributes?: Record; - createdAt: string; - updatedAt: string; -} - -/** - * Paginated user list response - */ -export interface IUserList { - data: IUserRead[]; - page: number; - perPage: number; - total: number; - totalPages: number; -} +/** + * User model representing an end-user in the authorization system + */ +export interface IUser { + key: string; + email?: string; + firstName?: string; + lastName?: string; + attributes?: Record; +} + +/** + * User creation payload + */ +export interface IUserCreate { + key: string; + email?: string; + firstName?: string; + lastName?: string; + attributes?: Record; +} + +/** + * User update payload + */ +export interface IUserUpdate { + email?: string; + firstName?: string; + lastName?: string; + attributes?: Record; +} + +/** + * User read response from API + */ +export interface IUserRead { + id: string; + key: string; + email?: string; + firstName?: string; + lastName?: string; + attributes?: Record; + createdAt: string; + updatedAt: string; +} + +/** + * Paginated user list response + */ +export interface IUserList { + data: IUserRead[]; + page: number; + perPage: number; + total: number; + totalPages: number; +} diff --git a/tsup.config.ts b/tsup.config.ts index a1aa7cc..e242321 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - format: ["cjs", "esm"], - dts: true, - splitting: false, - sourcemap: true, - clean: true, - minify: false, - treeshake: true, -}); +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + minify: false, + treeshake: true, +});