diff --git a/package.json b/package.json index 222baeb98..7be8b06fe 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,16 @@ "types": "./dist/conversational-agent/index.d.ts", "default": "./dist/conversational-agent/index.cjs" } + }, + "./policies": { + "import": { + "types": "./dist/policies/index.d.ts", + "default": "./dist/policies/index.mjs" + }, + "require": { + "types": "./dist/policies/index.d.ts", + "default": "./dist/policies/index.cjs" + } } }, "files": [ diff --git a/packages/cli/src/actions/deploy-policy.ts b/packages/cli/src/actions/deploy-policy.ts new file mode 100644 index 000000000..ce9d3ebf7 --- /dev/null +++ b/packages/cli/src/actions/deploy-policy.ts @@ -0,0 +1,168 @@ +import chalk from 'chalk'; +import ora from 'ora'; +import * as fs from 'fs'; +import * as path from 'path'; +import fetch from 'node-fetch'; +import type { EnvironmentConfig, PolicyConfig } from '../types/index.js'; +import { API_ENDPOINTS, AUTH_CONSTANTS } from '../constants/index.js'; +import { MESSAGES } from '../constants/messages.js'; +import { createHeaders } from '../utils/api.js'; +import { getEnvironmentConfig } from '../utils/env-config.js'; +import { handleHttpError } from '../utils/error-handler.js'; +import { cliTelemetryClient } from '../telemetry/index.js'; + +export interface DeployPolicyOptions { + policyId?: string; + baseUrl?: string; + orgId?: string; + tenantId?: string; + accessToken?: string; + logger?: { log: (message: string) => void }; +} + +interface TenantPolicy { + tenantIdentifier: string; + policyIdentifier: string | null; + productIdentifier: string; + licenseTypeIdentifier: string; + tenantName?: string; +} + +interface TenantResponse { + name: string; + identifier: string; + url: string; + status: string; + tenantPolicies: TenantPolicy[]; +} + +const AI_TRUST_LAYER_PRODUCT = 'AITrustLayer'; + +function loadPolicyConfig(logger: { log: (message: string) => void }): PolicyConfig | null { + const configPath = path.join(process.cwd(), AUTH_CONSTANTS.FILES.UIPATH_DIR, AUTH_CONSTANTS.FILES.POLICY_CONFIG); + try { + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, 'utf-8'); + return JSON.parse(content) as PolicyConfig; + } + } catch (error) { + logger.log(chalk.dim(`${MESSAGES.ERRORS.FAILED_TO_LOAD_POLICY_CONFIG} ${error instanceof Error ? error.message : ''}`)); + } + return null; +} + +async function getTenantPolicies( + tenantId: string, + envConfig: EnvironmentConfig +): Promise { + const url = `${envConfig.baseUrl}/${envConfig.orgId}/${API_ENDPOINTS.TENANT_GET.replace('{tenantId}', tenantId)}`; + + const response = await fetch(url, { + method: 'GET', + headers: createHeaders({ + bearerToken: envConfig.accessToken, + }), + }); + + if (!response.ok) { + await handleHttpError(response, MESSAGES.ERROR_CONTEXT.POLICY_DEPLOY); + } + + return (await response.json()) as TenantResponse; +} + +async function deployTenantPolicies( + policies: TenantPolicy[], + envConfig: EnvironmentConfig +): Promise { + const url = `${envConfig.baseUrl}/${envConfig.orgId}/${API_ENDPOINTS.TENANT_SAVE}`; + + // Remove tenantName from the payload as it's not needed for the POST request + const payload = policies.map(({ tenantName, ...policy }) => policy); + + const response = await fetch(url, { + method: 'POST', + headers: createHeaders({ + bearerToken: envConfig.accessToken, + }), + body: JSON.stringify(payload), + }); + + if (!response.ok) { + await handleHttpError(response, MESSAGES.ERROR_CONTEXT.POLICY_DEPLOY); + } +} + +export async function executeDeployPolicy(options: DeployPolicyOptions): Promise { + const logger = options.logger ?? { log: console.log }; + + logger.log(chalk.blue(MESSAGES.INFO.POLICY_DEPLOYING)); + + // Get policyId from options or load from stored config + let policyId = options.policyId; + if (!policyId) { + const policyConfig = loadPolicyConfig(logger); + if (policyConfig?.policyId) { + policyId = policyConfig.policyId; + logger.log(chalk.dim(`Using stored policy: ${policyConfig.policyName} (${policyId})`)); + } else { + throw new Error(MESSAGES.ERRORS.POLICY_ID_REQUIRED); + } + } + + const envConfig = getEnvironmentConfig( + AUTH_CONSTANTS.REQUIRED_ENV_VARS.DEPLOY_POLICY, + logger, + { + baseUrl: options.baseUrl, + orgId: options.orgId, + tenantId: options.tenantId, + accessToken: options.accessToken, + } + ); + if (!envConfig) throw new Error('Missing required configuration'); + + const spinner = ora(MESSAGES.INFO.FETCHING_TENANT_POLICIES).start(); + + try { + // Step 1: Get current tenant policies + const tenantData = await getTenantPolicies(envConfig.tenantId, envConfig); + + // Step 2: Find and update AITrustLayer policy + const updatedPolicies = tenantData.tenantPolicies.map((policy) => { + if (policy.productIdentifier === AI_TRUST_LAYER_PRODUCT) { + return { + ...policy, + policyIdentifier: policyId, + }; + } + return policy; + }); + + // Check if AITrustLayer policy was found + const aiTrustLayerPolicy = updatedPolicies.find( + (p) => p.productIdentifier === AI_TRUST_LAYER_PRODUCT + ); + if (!aiTrustLayerPolicy) { + spinner.fail(chalk.red(MESSAGES.ERRORS.AI_TRUST_LAYER_POLICY_NOT_FOUND)); + throw new Error(MESSAGES.ERRORS.AI_TRUST_LAYER_POLICY_NOT_FOUND); + } + + spinner.text = MESSAGES.INFO.DEPLOYING_POLICY_TO_TENANT; + + // Step 3: Deploy updated policies + await deployTenantPolicies(updatedPolicies, envConfig); + + spinner.succeed(chalk.green(MESSAGES.SUCCESS.POLICY_DEPLOYED_SUCCESS)); + cliTelemetryClient.track('Cli.DeployPolicy', { operation: 'deploy' }); + + logger.log(''); + logger.log(` ${chalk.cyan('Tenant:')} ${tenantData.name}`); + logger.log(` ${chalk.cyan('Tenant ID:')} ${tenantData.identifier}`); + logger.log(` ${chalk.cyan('Policy ID:')} ${policyId}`); + logger.log(` ${chalk.cyan('Product:')} ${AI_TRUST_LAYER_PRODUCT}`); + } catch (error) { + spinner.fail(chalk.red(MESSAGES.ERRORS.POLICY_DEPLOY_FAILED)); + throw error; + } +} diff --git a/packages/cli/src/actions/index.ts b/packages/cli/src/actions/index.ts index ff44ab894..aef7c5d1f 100644 --- a/packages/cli/src/actions/index.ts +++ b/packages/cli/src/actions/index.ts @@ -3,3 +3,5 @@ export { executePull, type PullOptions } from './pull.js'; export { executePack, type PackOptions } from './pack.js'; export { executePublish, type PublishOptions } from './publish.js'; export { executeDeploy, type DeployOptions } from './deploy.js'; +export { executePublishPolicy, type PublishPolicyOptions } from './publish-policy.js'; +export { executeDeployPolicy, type DeployPolicyOptions } from './deploy-policy.js'; diff --git a/packages/cli/src/actions/publish-policy.ts b/packages/cli/src/actions/publish-policy.ts new file mode 100644 index 000000000..dee1a7836 --- /dev/null +++ b/packages/cli/src/actions/publish-policy.ts @@ -0,0 +1,197 @@ +import chalk from 'chalk'; +import ora from 'ora'; +import * as fs from 'fs'; +import * as path from 'path'; +import fetch from 'node-fetch'; +import type { EnvironmentConfig, PolicyConfig } from '../types/index.js'; +import { API_ENDPOINTS, AUTH_CONSTANTS } from '../constants/index.js'; +import { MESSAGES } from '../constants/messages.js'; +import { createHeaders } from '../utils/api.js'; +import { getEnvironmentConfig, atomicWriteFileSync } from '../utils/env-config.js'; +import { handleHttpError } from '../utils/error-handler.js'; +import { cliTelemetryClient } from '../telemetry/index.js'; + +export interface PublishPolicyOptions { + file: string; + baseUrl?: string; + orgId?: string; + tenantId?: string; + accessToken?: string; + logger?: { log: (message: string) => void }; +} + +interface PolicyInputData { + 'policy-name': string; + 'product-name'?: string; + description?: string | null; + version?: string; + availability?: number; + data: Record; +} + +interface PolicyCreateResponse { + name: string; + identifier: string; + description: string | null; + priority: number; + availability: number; +} + +const HARDCODED_PRODUCT = { + name: 'AITrustLayer', + label: 'AI Trust Layer', + consumerProducts: [ + { + name: 'Business', + label: 'StudioX', + isRestricted: false, + isCloud: false, + isRemote: false, + }, + { + name: 'Development', + label: 'Studio', + isRestricted: false, + isCloud: false, + isRemote: false, + }, + { + name: 'StudioWeb', + label: 'Studio Web', + isRestricted: false, + isCloud: true, + isRemote: false, + }, + ], + isRestricted: false, + isCloud: true, + isRemote: false, +}; + +function loadPolicyFile(filePath: string, logger: { log: (message: string) => void }): PolicyInputData { + const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath); + + if (!fs.existsSync(absolutePath)) { + throw new Error(`${MESSAGES.ERRORS.POLICY_FILE_NOT_FOUND}: ${absolutePath}`); + } + + try { + const content = fs.readFileSync(absolutePath, 'utf-8'); + const parsed = JSON.parse(content) as PolicyInputData; + + if (!parsed['policy-name']) { + throw new Error(MESSAGES.ERRORS.POLICY_NAME_REQUIRED); + } + if (!parsed.data) { + throw new Error(MESSAGES.ERRORS.POLICY_DATA_REQUIRED); + } + + return parsed; + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error(`${MESSAGES.ERRORS.POLICY_FILE_INVALID_JSON}: ${error.message}`); + } + throw error; + } +} + +function buildPolicyPayload(policyInput: PolicyInputData): Record { + return { + policy: { + availability: policyInput.availability ?? 99, + name: policyInput['policy-name'], + priority: 4, + product: HARDCODED_PRODUCT, + description: policyInput.description ?? null, + }, + policyFormData: { + data: { + data: policyInput.data, + }, + }, + }; +} + +function savePolicyConfig(config: PolicyConfig, logger: { log: (message: string) => void }): void { + const configDir = path.join(process.cwd(), AUTH_CONSTANTS.FILES.UIPATH_DIR); + const configPath = path.join(configDir, AUTH_CONSTANTS.FILES.POLICY_CONFIG); + try { + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + atomicWriteFileSync(configPath, config); + } catch (error) { + logger.log(chalk.yellow(`${MESSAGES.ERRORS.FAILED_TO_SAVE_POLICY_CONFIG} ${error instanceof Error ? error.message : MESSAGES.ERRORS.UNKNOWN_ERROR}`)); + } +} + +async function createPolicy( + payload: Record, + envConfig: EnvironmentConfig +): Promise { + const url = `${envConfig.baseUrl}/${envConfig.orgId}/${API_ENDPOINTS.POLICY_SAVE}`; + + const response = await fetch(url, { + method: 'POST', + headers: createHeaders({ + bearerToken: envConfig.accessToken, + tenantId: envConfig.tenantId, + }), + body: JSON.stringify(payload), + }); + + if (!response.ok) { + await handleHttpError(response, MESSAGES.ERROR_CONTEXT.POLICY_PUBLISHING); + } + + return (await response.json()) as PolicyCreateResponse; +} + +export async function executePublishPolicy(options: PublishPolicyOptions): Promise { + const logger = options.logger ?? { log: console.log }; + + logger.log(chalk.blue(MESSAGES.INFO.POLICY_PUBLISHING)); + + const envConfig = getEnvironmentConfig( + AUTH_CONSTANTS.REQUIRED_ENV_VARS.PUBLISH_POLICY, + logger, + { + baseUrl: options.baseUrl, + orgId: options.orgId, + tenantId: options.tenantId, + accessToken: options.accessToken, + } + ); + if (!envConfig) throw new Error('Missing required configuration'); + + const spinner = ora(MESSAGES.INFO.READING_POLICY_FILE).start(); + + try { + const policyInput = loadPolicyFile(options.file, logger); + spinner.text = MESSAGES.INFO.PUBLISHING_POLICY; + + const payload = buildPolicyPayload(policyInput); + const result = await createPolicy(payload, envConfig); + + spinner.succeed(chalk.green(MESSAGES.SUCCESS.POLICY_PUBLISHED_SUCCESS)); + cliTelemetryClient.track('Cli.PublishPolicy', { operation: 'create' }); + + // Save policy config for later use by deploy-policy + savePolicyConfig({ + policyId: result.identifier, + policyName: result.name, + publishedAt: new Date().toISOString(), + }, logger); + + logger.log(''); + logger.log(` ${chalk.cyan('Policy Name:')} ${result.name}`); + logger.log(` ${chalk.cyan('Policy ID:')} ${result.identifier}`); + logger.log(` ${chalk.cyan('Availability:')} ${result.availability}%`); + if (result.description) { + logger.log(` ${chalk.cyan('Description:')} ${result.description}`); + } + } catch (error) { + spinner.fail(chalk.red(MESSAGES.ERRORS.POLICY_PUBLISHING_FAILED)); + throw error; + } +} diff --git a/packages/cli/src/auth/utils/url.ts b/packages/cli/src/auth/utils/url.ts index 2454d7750..0a90e9ecd 100644 --- a/packages/cli/src/auth/utils/url.ts +++ b/packages/cli/src/auth/utils/url.ts @@ -1,7 +1,7 @@ import { BASE_URLS, AUTH_CONSTANTS } from '../../constants/auth.js'; export const getBaseUrl = (domain: string): string => { - return BASE_URLS[domain] || BASE_URLS.cloud; + return BASE_URLS[domain] || BASE_URLS.alpha; }; export const buildRedirectUri = (port: number): string => { diff --git a/packages/cli/src/commands/auth.ts b/packages/cli/src/commands/auth.ts index cc47e2356..e4da5fd63 100644 --- a/packages/cli/src/commands/auth.ts +++ b/packages/cli/src/commands/auth.ts @@ -29,8 +29,8 @@ export default class Auth extends Command { domain: Flags.string({ char: 'd', description: 'UiPath domain to authenticate with', - options: [AUTH_CONSTANTS.DOMAINS.CLOUD, AUTH_CONSTANTS.DOMAINS.ALPHA, AUTH_CONSTANTS.DOMAINS.STAGING], - default: AUTH_CONSTANTS.DOMAINS.CLOUD, + options: [AUTH_CONSTANTS.DOMAINS.ALPHA, AUTH_CONSTANTS.DOMAINS.CLOUD, AUTH_CONSTANTS.DOMAINS.STAGING], + default: AUTH_CONSTANTS.DOMAINS.ALPHA, }), alpha: createDomainShorthandFlag('alpha', ['cloud', 'staging']), cloud: createDomainShorthandFlag('cloud', ['alpha', 'staging']), diff --git a/packages/cli/src/commands/deploy-policy.ts b/packages/cli/src/commands/deploy-policy.ts new file mode 100644 index 000000000..e8730275c --- /dev/null +++ b/packages/cli/src/commands/deploy-policy.ts @@ -0,0 +1,46 @@ +import { Command, Flags } from '@oclif/core'; +import chalk from 'chalk'; +import { MESSAGES } from '../constants/messages.js'; +import { track } from '../telemetry/index.js'; +import { executeDeployPolicy } from '../actions/deploy-policy.js'; +import { COMMON_FLAGS } from '../utils/flags.js'; + +export default class DeployPolicy extends Command { + static override description = 'Deploy an AI Trust Layer policy to a tenant'; + + static override examples = [ + '<%= config.bin %> <%= command.id %> --policyId b76dd7ea-cf0c-4222-96ea-0f01501cf851', + '<%= config.bin %> <%= command.id %> -p b76dd7ea-cf0c-4222-96ea-0f01501cf851', + "<%= config.bin %> <%= command.id %> --policyId 'b76dd7ea-cf0c-4222-96ea-0f01501cf851' --orgId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' --tenantId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' --accessToken 'your_token'", + ]; + + static override flags = { + help: Flags.help({ char: 'h' }), + policyId: Flags.string({ + char: 'p', + description: 'Policy ID to deploy (uses stored policy from publish-policy if not provided)', + required: false, + }), + ...COMMON_FLAGS, + }; + + @track('DeployPolicy') + public async run(): Promise { + const { flags } = await this.parse(DeployPolicy); + try { + await executeDeployPolicy({ + policyId: flags.policyId, + baseUrl: flags.baseUrl, + orgId: flags.orgId, + tenantId: flags.tenantId, + accessToken: flags.accessToken, + logger: this, + }); + process.exit(0); + } catch (error) { + const msg = error instanceof Error ? error.message : MESSAGES.ERRORS.UNKNOWN_ERROR; + this.log(chalk.red(MESSAGES.ERRORS.POLICY_DEPLOY_ERROR_PREFIX + msg)); + process.exit(1); + } + } +} diff --git a/packages/cli/src/commands/publish-policy.ts b/packages/cli/src/commands/publish-policy.ts new file mode 100644 index 000000000..e3ad616b2 --- /dev/null +++ b/packages/cli/src/commands/publish-policy.ts @@ -0,0 +1,46 @@ +import { Command, Flags } from '@oclif/core'; +import chalk from 'chalk'; +import { MESSAGES } from '../constants/messages.js'; +import { track } from '../telemetry/index.js'; +import { executePublishPolicy } from '../actions/publish-policy.js'; +import { COMMON_FLAGS } from '../utils/flags.js'; + +export default class PublishPolicy extends Command { + static override description = 'Publish an AI Trust Layer policy to UiPath'; + + static override examples = [ + '<%= config.bin %> <%= command.id %> --file ./ai-trust-policies/my-policy.json', + '<%= config.bin %> <%= command.id %> -f ./policy.json', + "<%= config.bin %> <%= command.id %> --file './ai-trust-policies/enterprise-policy.json' --orgId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' --tenantId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' --accessToken 'your_token'", + ]; + + static override flags = { + help: Flags.help({ char: 'h' }), + file: Flags.string({ + char: 'f', + description: 'Path to the policy JSON file', + required: true, + }), + ...COMMON_FLAGS, + }; + + @track('PublishPolicy') + public async run(): Promise { + const { flags } = await this.parse(PublishPolicy); + try { + await executePublishPolicy({ + file: flags.file, + baseUrl: flags.baseUrl, + orgId: flags.orgId, + tenantId: flags.tenantId, + accessToken: flags.accessToken, + logger: this, + }); + process.exit(0); + } catch (error) { + const msg = error instanceof Error ? error.message : MESSAGES.ERRORS.UNKNOWN_ERROR; + this.log(chalk.red(MESSAGES.ERRORS.POLICY_PUBLISHING_ERROR_PREFIX + msg)); + process.exit(1); + } + } +} diff --git a/packages/cli/src/commands/publish.ts b/packages/cli/src/commands/publish.ts index 00e17e267..47a829a42 100644 --- a/packages/cli/src/commands/publish.ts +++ b/packages/cli/src/commands/publish.ts @@ -32,7 +32,7 @@ export default class Publish extends Command { default: './.uipath', }), baseUrl: Flags.string({ - description: 'UiPath base URL (default: https://cloud.uipath.com)', + description: 'UiPath base URL (default: https://alpha.uipath.com)', }), orgId: Flags.string({ description: 'UiPath organization ID', diff --git a/packages/cli/src/commands/pull.ts b/packages/cli/src/commands/pull.ts index e0b6750b2..f9f51c962 100644 --- a/packages/cli/src/commands/pull.ts +++ b/packages/cli/src/commands/pull.ts @@ -35,7 +35,7 @@ export default class Pull extends Command { 'Local directory to write pulled files; should be the root of the app project (default: current working directory)', }), baseUrl: Flags.string({ - description: 'UiPath base URL (default: https://cloud.uipath.com)', + description: 'UiPath base URL (default: https://alpha.uipath.com)', }), orgId: Flags.string({ description: 'UiPath organization ID', diff --git a/packages/cli/src/commands/push.ts b/packages/cli/src/commands/push.ts index a90d908fa..b01ec7873 100644 --- a/packages/cli/src/commands/push.ts +++ b/packages/cli/src/commands/push.ts @@ -34,7 +34,7 @@ export default class Push extends Command { default: false, }), baseUrl: Flags.string({ - description: 'UiPath base URL (default: https://cloud.uipath.com)', + description: 'UiPath base URL (default: https://alpha.uipath.com)', }), orgId: Flags.string({ description: 'UiPath organization ID', diff --git a/packages/cli/src/constants/api.ts b/packages/cli/src/constants/api.ts index 5f301c4cd..46553e933 100644 --- a/packages/cli/src/constants/api.ts +++ b/packages/cli/src/constants/api.ts @@ -1,4 +1,8 @@ export const API_ENDPOINTS = { + // AI Trust Layer Policy endpoints + POLICY_SAVE: 'roboticsops_/api/Policy', + TENANT_GET: 'roboticsops_/api/Tenant/{tenantId}', + TENANT_SAVE: 'roboticsops_/api/Tenant', PUBLISH_CODED_APP: '/apps_/default/api/v1/default/models/apps/codedapp/publish', UPLOAD_PACKAGE: '/orchestrator_/odata/Processes/UiPath.Server.Configuration.OData.UploadPackage()', // Base: /studio_/backend/api/Project/{project_id}/FileOperations diff --git a/packages/cli/src/constants/auth.ts b/packages/cli/src/constants/auth.ts index db29fab3b..6366a7940 100644 --- a/packages/cli/src/constants/auth.ts +++ b/packages/cli/src/constants/auth.ts @@ -3,6 +3,7 @@ export const AUTH_CONSTANTS = { FILES: { UIPATH_DIR: '.uipath', APP_CONFIG: 'app.config.json', + POLICY_CONFIG: 'policy.config.json', AUTH_FILE: '.auth.json', ENV_FILE: '.env', METADATA_FILE: 'metadata.json', @@ -74,7 +75,7 @@ export const AUTH_CONSTANTS = { altEnvVar: 'UIPATH_URL', configKey: 'baseUrl' as const, flag: '--baseUrl', - example: "'https://cloud.uipath.com'", + example: "'https://alpha.uipath.com'", }, ORG_ID: { envVar: 'UIPATH_ORG_ID', @@ -153,6 +154,20 @@ export const AUTH_CONSTANTS = { 'UIPATH_TENANT_ID', 'UIPATH_ACCESS_TOKEN' ], + // Required for publish-policy command (AI Trust Layer) + PUBLISH_POLICY: [ + 'UIPATH_BASE_URL', + 'UIPATH_ORG_ID', + 'UIPATH_TENANT_ID', + 'UIPATH_ACCESS_TOKEN' + ], + // Required for deploy-policy command (AI Trust Layer) + DEPLOY_POLICY: [ + 'UIPATH_BASE_URL', + 'UIPATH_ORG_ID', + 'UIPATH_TENANT_ID', + 'UIPATH_ACCESS_TOKEN' + ], }, API_ENDPOINTS: { FOLDERS_NAVIGATION: '/Folders/GetAllForCurrentUser', diff --git a/packages/cli/src/constants/messages.ts b/packages/cli/src/constants/messages.ts index 4a344fa14..151c6b209 100644 --- a/packages/cli/src/constants/messages.ts +++ b/packages/cli/src/constants/messages.ts @@ -146,7 +146,23 @@ export const MESSAGES = { INVALID_PROPERTIES_OBJECT: 'Properties must be a valid object', MISSING_ACTION_SCHEMA_SECTION: 'Action schema must have inputs, outputs, inOuts, and outcomes sections', INVALID_ACTION_SCHEMA: 'Action schema validation failed:', - UNSUPPORTED_JSON_DATA_TYPE: 'Unsupported JSON data type:' + UNSUPPORTED_JSON_DATA_TYPE: 'Unsupported JSON data type:', + + // Policy publishing + POLICY_PUBLISHING_ERROR_PREFIX: 'Policy publishing error: ', + POLICY_PUBLISHING_FAILED: '❌ Policy publishing failed', + POLICY_FILE_NOT_FOUND: '❌ Policy file not found', + POLICY_FILE_INVALID_JSON: '❌ Policy file is not valid JSON', + POLICY_NAME_REQUIRED: '❌ Policy file must contain a "policy-name" field', + POLICY_DATA_REQUIRED: '❌ Policy file must contain a "data" field', + FAILED_TO_SAVE_POLICY_CONFIG: 'Failed to save policy configuration:', + + // Policy deployment + POLICY_DEPLOY_ERROR_PREFIX: 'Policy deployment error: ', + POLICY_DEPLOY_FAILED: '❌ Policy deployment failed', + AI_TRUST_LAYER_POLICY_NOT_FOUND: '❌ AITrustLayer policy not found in tenant policies', + POLICY_ID_REQUIRED: '❌ Policy ID is required. Provide --policyId flag or run "uipath publish-policy" first', + FAILED_TO_LOAD_POLICY_CONFIG: 'Failed to load policy config:' }, @@ -174,6 +190,12 @@ export const MESSAGES = { APP_DEPLOYED_SUCCESS: '✅ App deployed successfully!', APP_UPGRADED_SUCCESS: '✅ App upgraded successfully!', + // Policy publishing + POLICY_PUBLISHED_SUCCESS: '✅ Policy published successfully!', + + // Policy deployment + POLICY_DEPLOYED_SUCCESS: '✅ Policy deployed to tenant successfully!', + // Push PUSH_COMPLETED: 'Push completed successfully.', @@ -236,6 +258,12 @@ export const MESSAGES = { PUSH_RESOURCE_UPDATED_PREFIX: '[resources] Updated: ', APP_REGISTRATION: '🚀 UiPath App Registration', APP_DEPLOYMENT: '🚀 UiPath App Deployment', + POLICY_PUBLISHING: '🚀 UiPath Policy Publishing', + READING_POLICY_FILE: 'Reading policy file...', + PUBLISHING_POLICY: 'Publishing policy to AI Trust Layer...', + POLICY_DEPLOYING: '🚀 UiPath Policy Deployment', + FETCHING_TENANT_POLICIES: 'Fetching tenant policies...', + DEPLOYING_POLICY_TO_TENANT: 'Deploying policy to tenant...', PACKAGE_CREATOR: '📦 UiPath NuGet Package Creator', PUBLISHER: '🚀 UiPath Publisher', PACKAGE_PREVIEW: '🔍 Package Preview', @@ -288,6 +316,8 @@ export const MESSAGES = { PUSH_ACQUIRE_LOCK: 'acquire lock', PUSH_RELEASE_LOCK: 'release lock', PUSH_PULL_OPERATION: 'push/pull operation', + POLICY_PUBLISHING: 'policy publishing', + POLICY_DEPLOY: 'policy deployment', }, } as const; diff --git a/packages/cli/src/types/config.ts b/packages/cli/src/types/config.ts index 39b3a5d70..e5e58d19d 100644 --- a/packages/cli/src/types/config.ts +++ b/packages/cli/src/types/config.ts @@ -33,6 +33,14 @@ export interface AppConfig { appType?: string; } +export interface PolicyConfig { + policyId: string; + policyName: string; + publishedAt: string; + deployedAt?: string; + tenantId?: string; +} + /** * SDK configuration type for uipath.json * Note: scope is optional when clientId is provided (uses client's registered scopes) diff --git a/packages/cli/src/utils/env-config.ts b/packages/cli/src/utils/env-config.ts index 5c047258c..bd7847006 100644 --- a/packages/cli/src/utils/env-config.ts +++ b/packages/cli/src/utils/env-config.ts @@ -135,7 +135,7 @@ function logMissingConfigError( * Normalizes base URL to ensure it has a protocol */ function normalizeBaseUrl(url: string | undefined): string { - let baseUrl = url || BASE_URLS.cloud; + let baseUrl = url || BASE_URLS.alpha; if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) { baseUrl = `https://${baseUrl}`; } diff --git a/packages/cli/src/utils/flags.ts b/packages/cli/src/utils/flags.ts index f1afded69..8a37f22a0 100644 --- a/packages/cli/src/utils/flags.ts +++ b/packages/cli/src/utils/flags.ts @@ -5,7 +5,7 @@ import { Flags } from '@oclif/core'; */ export const COMMON_FLAGS = { baseUrl: Flags.string({ - description: 'UiPath base URL (default: https://cloud.uipath.com)', + description: 'UiPath base URL (default: https://alpha.uipath.com)', }), orgId: Flags.string({ description: 'UiPath organization ID', diff --git a/rollup.config.js b/rollup.config.js index 571c396d2..eb7813bfe 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -183,6 +183,11 @@ const serviceEntries = [ name: 'conversational-agent', input: 'src/services/conversational-agent/index.ts', output: 'conversational-agent/index' + }, + { + name: 'policies', + input: 'src/services/aops/index.ts', + output: 'policies/index' } ]; diff --git a/src/core/http/api-client.ts b/src/core/http/api-client.ts index dac52bac3..3ec86da45 100644 --- a/src/core/http/api-client.ts +++ b/src/core/http/api-client.ts @@ -5,6 +5,7 @@ import { TokenManager } from '../auth/token-manager'; import { errorResponseParser } from '../errors/parser'; import { ErrorFactory } from '../errors/error-factory'; import { CONTENT_TYPES, RESPONSE_TYPES } from '../../utils/constants/headers'; +import { AOPS_BASE } from '../../utils/constants/endpoints/base'; export interface ApiClientConfig { headers?: Record; @@ -58,12 +59,15 @@ export class ApiClient { private async request(method: string, path: string, options: RequestSpec = {}): Promise { // Ensure path starts with a forward slash const normalizedPath = path.startsWith('/') ? path.substring(1) : path; - - // Construct URL with org and tenant names - const url = new URL( - `${this.config.orgName}/${this.config.tenantName}/${normalizedPath}`, - this.config.baseUrl - ).toString(); + + // Aops endpoints don't require tenant in the URL + const isAopsPath = normalizedPath.startsWith(AOPS_BASE); + + // Construct URL with org and tenant names (skip tenant for Aops) + const basePath = isAopsPath + ? `${this.config.orgName}/${normalizedPath}` + : `${this.config.orgName}/${this.config.tenantName}/${normalizedPath}`; + const url = new URL(basePath, this.config.baseUrl).toString(); const isFormData = options.body instanceof FormData; const defaultHeaders = await this.getDefaultHeaders(); diff --git a/src/models/aops/index.ts b/src/models/aops/index.ts new file mode 100644 index 000000000..2defe1e7f --- /dev/null +++ b/src/models/aops/index.ts @@ -0,0 +1,6 @@ +export * from './policies.types'; +export * from './policies.models'; +export * from './tenants.types'; +export * from './tenants.models'; +export * from './license-types.types'; +export * from './license-types.models'; diff --git a/src/models/aops/license-types.models.ts b/src/models/aops/license-types.models.ts new file mode 100644 index 000000000..6ced61851 --- /dev/null +++ b/src/models/aops/license-types.models.ts @@ -0,0 +1,17 @@ +import { LicenseType } from './license-types.types'; + +/** + * Response type for getting all license types + * The API returns an array of LicenseType objects directly + */ +export type LicenseTypeGetAllResponse = LicenseType[]; + +/** + * Service model interface for LicenseType operations + */ +export interface LicenseTypeServiceModel { + /** + * Gets all license types + */ + getAll(): Promise; +} diff --git a/src/models/aops/license-types.types.ts b/src/models/aops/license-types.types.ts new file mode 100644 index 000000000..354612b43 --- /dev/null +++ b/src/models/aops/license-types.types.ts @@ -0,0 +1,23 @@ +import { Product } from './policies.types'; + +/** + * Product information associated with a license type + */ +export interface LicenseTypeProduct { + /** Product details */ + product: Product; +} + +/** + * License type definition + */ +export interface LicenseType { + /** License type name identifier */ + name: string; + /** Human-readable license type label */ + label: string; + /** Products associated with this license type */ + licenseTypeProducts: LicenseTypeProduct[]; + /** Whether the license type is restricted */ + isRestricted: boolean; +} diff --git a/src/models/aops/policies.constants.ts b/src/models/aops/policies.constants.ts new file mode 100644 index 000000000..7fa297e3a --- /dev/null +++ b/src/models/aops/policies.constants.ts @@ -0,0 +1,29 @@ +/** + * Field mapping for policy response transformations + * Maps API field names to SDK field names for consistency + */ +export const PolicyMap: Record = { + // Add any field mappings if needed in the future + // Example: 'ApiFieldName': 'sdkFieldName' +}; + +/** + * Default page size for policy pagination + */ +export const DEFAULT_POLICY_PAGE_SIZE = 100; + +/** + * Policy pagination configuration + */ +export const POLICY_PAGINATION = { + ITEMS_FIELD: 'result', + TOTAL_COUNT_FIELD: 'totalCount', +} as const; + +/** + * Policy pagination parameters + */ +export const POLICY_PAGINATION_PARAMS = { + PAGE_SIZE_PARAM: 'pageSize', + PAGE_INDEX_PARAM: 'pageIndex', +} as const; diff --git a/src/models/aops/policies.models.ts b/src/models/aops/policies.models.ts new file mode 100644 index 000000000..1cc633351 --- /dev/null +++ b/src/models/aops/policies.models.ts @@ -0,0 +1,59 @@ +import { Policy, Product } from './policies.types'; + +/** + * Response from getting a single policy + */ +export type PolicyGetResponse = Policy; + +/** + * Response from getting all policies (paginated) + */ +export interface PolicyPagedResponse { + /** Total number of policies */ + totalCount: number; + /** Array of policies in the current page */ + result: Policy[]; +} + +/** + * Response from getting policy details (policy + form data) + */ +export interface PolicyDetailsResponse { + /** The policy metadata */ + policy: Policy; + /** The policy form/configuration data */ + formData: Record; +} + +/** + * Response from creating a policy + */ +export type PolicyCreateResponse = Policy; + +/** + * Response from getting products + */ +export type ProductGetResponse = Product[]; + +/** + * Response from getting a form template + */ +export type FormTemplateResponse = Record; + +/** + * Service model interface for PolicyService + */ +export interface PolicyServiceModel { + getAll(options?: import('./policies.types').PolicyGetAllOptions): Promise< + import('../../utils/pagination/types').PaginatedResponse | + import('../../utils/pagination/types').NonPaginatedResponse + >; + getById(policyId: string): Promise; + getFormData(policyId: string): Promise>; + getDetails(policyId: string): Promise; + getProducts(): Promise; + getFormTemplate(productName: string): Promise; + create(options: import('./policies.types').PolicyCreateOptions): Promise; + update(options: import('./policies.types').PolicyUpdateOptions): Promise; + deleteById(policyId: string): Promise; +} diff --git a/src/models/aops/policies.types.ts b/src/models/aops/policies.types.ts new file mode 100644 index 000000000..d83719452 --- /dev/null +++ b/src/models/aops/policies.types.ts @@ -0,0 +1,109 @@ +import { PaginationOptions } from '../../utils/pagination/types'; + +/** + * Tenant policy assignment + */ +export interface TenantPolicy { + tenantIdentifier: string; + policyIdentifier: string; + productIdentifier: string; + licenseTypeIdentifier: string; + tenantName: string; +} + +/** + * User policy assignment + */ +export interface UserPolicy { + userId: string; + userName: string; + policyIdentifier: string; + productIdentifier: string; +} + +/** + * Group policy assignment + */ +export interface GroupPolicy { + groupId: string; + groupName: string; + policyIdentifier: string; + productIdentifier: string; +} + +/** + * Product associated with a policy + */ +export interface Product { + name: string; + label?: string; + isRestricted?: boolean; + isCloud?: boolean; + isRemote?: boolean; +} + +/** + * Policy entity + */ +export interface Policy { + /** Policy name */ + name: string; + /** Unique policy identifier */ + identifier: string; + /** Product associated with the policy */ + product: Product; + /** Policy description */ + description: string | null; + /** Policy priority (lower = higher priority) */ + priority: number; + /** Policy availability (0-100) */ + availability: number; + /** Tenant policy assignments */ + tenantPolicies: TenantPolicy[]; + /** User policy assignments */ + userPolicies: UserPolicy[]; + /** Group policy assignments */ + groupPolicies: GroupPolicy[]; +} + +/** + * Policy form data structure + */ +export interface PolicyFormData { + data: { + data: Record; + }; +} + +/** + * Options for getting all policies + */ +export type PolicyGetAllOptions = PaginationOptions; + +/** + * Options for creating a new policy + */ +export interface PolicyCreateOptions { + /** Policy name */ + name: string; + /** Product name for the policy */ + productName: string; + /** Policy configuration data */ + data: Record; + /** Optional policy description */ + description?: string | null; + /** Policy availability (0-100, default: 99) */ + availability?: number; +} + +/** + * Options for updating an existing policy + */ +export interface PolicyUpdateOptions { + /** Policy identifier to update */ + policyId: string; + /** Updated policy configuration data */ + data: Record; + /** Policy metadata to update */ + policy: Pick; +} diff --git a/src/models/aops/tenants.constants.ts b/src/models/aops/tenants.constants.ts new file mode 100644 index 000000000..b15e2a4cf --- /dev/null +++ b/src/models/aops/tenants.constants.ts @@ -0,0 +1,23 @@ +/** + * Default pagination settings for tenant API + */ +export const TENANT_PAGINATION = { + /** Default page size for tenant listing */ + DEFAULT_PAGE_SIZE: 100, + /** Field name for items array in response */ + ITEMS_FIELD: 'items', + /** Field name for total count in response */ + TOTAL_COUNT_FIELD: 'totalRecordCount', +} as const; + +/** + * Pagination parameter names for tenant API + */ +export const TENANT_PAGINATION_PARAMS = { + /** Parameter name for page size */ + PAGE_SIZE_PARAM: 'pageSize', + /** Parameter name for page index */ + PAGE_INDEX_PARAM: 'pageIndex', + /** Parameter name for search term */ + SEARCH_TERM_PARAM: 'searchTerm', +} as const; diff --git a/src/models/aops/tenants.models.ts b/src/models/aops/tenants.models.ts new file mode 100644 index 000000000..8147b1718 --- /dev/null +++ b/src/models/aops/tenants.models.ts @@ -0,0 +1,55 @@ +import { Tenant, TenantGetAllOptions, TenantPolicyAssignment, TenantPolicyUpdateOptions } from './tenants.types'; +import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination/types'; + +/** + * Response type for getting a single tenant + */ +export type TenantGetResponse = Tenant; + +/** + * Response type for getting all tenants (paginated) + */ +export interface TenantPagedResponse { + /** Array of tenants */ + result: Tenant[]; + /** Total number of tenants */ + totalCount: number; +} + +/** + * Request body for getting all tenants (PUT request) + */ +export interface TenantGetAllRequest { + /** Page index (0-based) */ + pageIndex: number; + /** Number of items per page */ + pageSize: number; + /** Search term to filter tenants */ + searchTerm: string; +} + +/** + * Service model interface for Tenant operations + */ +export interface TenantServiceModel { + /** + * Gets all tenants with optional pagination + */ + getAll( + options?: T + ): Promise< + T extends HasPaginationOptions + ? PaginatedResponse + : NonPaginatedResponse + >; + + /** + * Gets a tenant by its identifier + */ + getById(tenantId: string): Promise; + + /** + * Updates tenant policies + */ + updatePolicies(options: TenantPolicyUpdateOptions): Promise; +} diff --git a/src/models/aops/tenants.types.ts b/src/models/aops/tenants.types.ts new file mode 100644 index 000000000..35bd6a816 --- /dev/null +++ b/src/models/aops/tenants.types.ts @@ -0,0 +1,49 @@ +import { PaginationOptions } from '../../utils/pagination/types'; + +/** + * Tenant policy assignment in a tenant + */ +export interface TenantPolicyAssignment { + /** Tenant identifier */ + tenantIdentifier: string; + /** Policy identifier (null if no policy assigned) */ + policyIdentifier: string | null; + /** Product identifier */ + productIdentifier: string; + /** License type identifier */ + licenseTypeIdentifier: string; + /** Tenant name (optional, returned in GET response) */ + tenantName?: string; +} + +/** + * Tenant entity returned from the API + */ +export interface Tenant { + /** Tenant name */ + name: string; + /** Tenant unique identifier */ + identifier: string; + /** Tenant URL */ + url: string; + /** Tenant status */ + status: string; + /** Tenant policy assignments */ + tenantPolicies: TenantPolicyAssignment[]; +} + +/** + * Options for getting all tenants + */ +export type TenantGetAllOptions = PaginationOptions & { + /** Search term to filter tenants */ + searchTerm?: string; +}; + +/** + * Options for updating tenant policies + */ +export interface TenantPolicyUpdateOptions { + /** Array of tenant policy assignments to update */ + policies: TenantPolicyAssignment[]; +} diff --git a/src/models/index.ts b/src/models/index.ts index 5f8d3110d..9b2d22060 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -12,3 +12,6 @@ export * from './orchestrator'; // Task models export * from './action-center'; + +// AoPS (Autonomous Operations) models +export * from './aops'; diff --git a/src/services/aops/index.ts b/src/services/aops/index.ts new file mode 100644 index 000000000..ec2875c33 --- /dev/null +++ b/src/services/aops/index.ts @@ -0,0 +1,45 @@ +/** + * AoPS (Autonomous Operations) Services Module + * + * Provides access to UiPath Autonomous Operations services for policy management. + * + * @example + * ```typescript + * import { UiPath } from '@uipath/uipath-typescript/core'; + * import { Policies } from '@uipath/uipath-typescript/policies'; + * + * const sdk = new UiPath(config); + * await sdk.initialize(); + * + * const policies = new Policies(sdk); + * + * // Get all policies + * const allPolicies = await policies.getAll(); + * + * // Get policy details + * const details = await policies.getDetails('policy-id'); + * + * // Create a new policy + * const template = await policies.getFormTemplate('AITrustLayer'); + * const newPolicy = await policies.create({ + * name: 'My Policy', + * productName: 'AITrustLayer', + * data: template + * }); + * ``` + * + * @module + */ + +// Export service with cleaner name and keep PolicyService for legacy UiPath class +export { PolicyService as Policies, PolicyService } from './policies'; +export { TenantService as Tenants, TenantService } from './tenants'; +export { LicenseTypeService as LicenseTypes, LicenseTypeService } from './license-types'; + +// Re-export service-specific types +export * from '../../models/aops/policies.types'; +export * from '../../models/aops/policies.models'; +export * from '../../models/aops/tenants.types'; +export * from '../../models/aops/tenants.models'; +export * from '../../models/aops/license-types.types'; +export * from '../../models/aops/license-types.models'; diff --git a/src/services/aops/license-types.ts b/src/services/aops/license-types.ts new file mode 100644 index 000000000..83bf1af50 --- /dev/null +++ b/src/services/aops/license-types.ts @@ -0,0 +1,54 @@ +import { BaseService } from '../base'; +import { LicenseType } from '../../models/aops/license-types.types'; +import { + LicenseTypeServiceModel, + LicenseTypeGetAllResponse, +} from '../../models/aops/license-types.models'; +import { LICENSE_TYPE_ENDPOINTS } from '../../utils/constants/endpoints/aops'; +import { track } from '../../core/telemetry'; + +/** + * Service for interacting with UiPath License Type API + * + * Provides methods for retrieving license types available in the + * UiPath Autonomous Operations (AoPS) platform. + * + * @example + * ```typescript + * import { UiPath } from '@uipath/uipath-typescript/core'; + * import { LicenseTypes } from '@uipath/uipath-typescript/policies'; + * + * const sdk = new UiPath(config); + * await sdk.initialize(); + * + * const licenseTypes = new LicenseTypes(sdk); + * + * // Get all license types + * const allLicenseTypes = await licenseTypes.getAll(); + * ``` + */ +export class LicenseTypeService extends BaseService implements LicenseTypeServiceModel { + /** + * Gets all license types + * + * @returns Promise resolving to array of license types + * + * @example + * ```typescript + * import { LicenseTypes } from '@uipath/uipath-typescript/policies'; + * + * const licenseTypes = new LicenseTypes(sdk); + * + * // Get all license types + * const allLicenseTypes = await licenseTypes.getAll(); + * console.log(allLicenseTypes.map(lt => lt.label)); + * ``` + */ + @track('LicenseTypes.GetAll') + async getAll(): Promise { + const response = await this.get( + LICENSE_TYPE_ENDPOINTS.GET_ALL + ); + return response.data; + } +} diff --git a/src/services/aops/policies.ts b/src/services/aops/policies.ts new file mode 100644 index 000000000..7a06a87c8 --- /dev/null +++ b/src/services/aops/policies.ts @@ -0,0 +1,315 @@ +import { BaseService } from '../base'; +import { + Policy, + PolicyGetAllOptions, + PolicyCreateOptions, + PolicyUpdateOptions, +} from '../../models/aops/policies.types'; +import { + PolicyServiceModel, + PolicyGetResponse, + PolicyPagedResponse, + PolicyDetailsResponse, + PolicyCreateResponse, + ProductGetResponse, + FormTemplateResponse, +} from '../../models/aops/policies.models'; +import { + POLICY_PAGINATION, + POLICY_PAGINATION_PARAMS, +} from '../../models/aops/policies.constants'; +import { POLICY_ENDPOINTS, PRODUCT_ENDPOINTS, CONTENT_ENDPOINTS } from '../../utils/constants/endpoints/aops'; +import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination/types'; +import { PaginationType } from '../../utils/pagination/internal-types'; +import { PaginationHelpers } from '../../utils/pagination/helpers'; +import { track } from '../../core/telemetry'; + +/** + * Service for interacting with UiPath Policy API + * + * Provides methods for managing policies, products, and form templates + * in the UiPath Autonomous Operations (AoPS) platform. + * + * @example + * ```typescript + * import { UiPath } from '@uipath/uipath-typescript/core'; + * import { Policies } from '@uipath/uipath-typescript/policies'; + * + * const sdk = new UiPath(config); + * await sdk.initialize(); + * + * const policies = new Policies(sdk); + * + * // Get all policies + * const allPolicies = await policies.getAll(); + * + * // Get a specific policy with its form data + * const details = await policies.getDetails('policy-id'); + * ``` + */ +export class PolicyService extends BaseService implements PolicyServiceModel { + /** + * Gets all policies with optional pagination + * + * @param options - Pagination options (pageSize, cursor, jumpToPage) + * @returns Promise resolving to array of policies or paginated response + * + * @example + * ```typescript + * import { Policies } from '@uipath/uipath-typescript/policies'; + * + * const policies = new Policies(sdk); + * + * // Get all policies (non-paginated) + * const allPolicies = await policies.getAll(); + * + * // Get first page with pagination + * const page1 = await policies.getAll({ pageSize: 10 }); + * + * // Navigate to next page + * if (page1.hasNextPage) { + * const page2 = await policies.getAll({ cursor: page1.nextCursor }); + * } + * + * // Jump to specific page + * const page5 = await policies.getAll({ jumpToPage: 5, pageSize: 10 }); + * ``` + */ + @track('Policies.GetAll') + async getAll( + options?: T + ): Promise< + T extends HasPaginationOptions + ? PaginatedResponse + : NonPaginatedResponse + > { + return PaginationHelpers.getAll({ + serviceAccess: this.createPaginationServiceAccess(), + getEndpoint: () => POLICY_ENDPOINTS.GET_ALL, + pagination: { + paginationType: PaginationType.OFFSET, + itemsField: POLICY_PAGINATION.ITEMS_FIELD, + totalCountField: POLICY_PAGINATION.TOTAL_COUNT_FIELD, + paginationParams: { + pageSizeParam: POLICY_PAGINATION_PARAMS.PAGE_SIZE_PARAM, + offsetParam: POLICY_PAGINATION_PARAMS.PAGE_INDEX_PARAM, + }, + }, + }, options) as any; + } + + /** + * Gets a policy by its identifier + * + * @param policyId - The unique identifier of the policy + * @returns Promise resolving to the policy + * + * @example + * ```typescript + * const policy = await policies.getById('my-policy-id'); + * console.log(policy.name, policy.product.name); + * ``` + */ + @track('Policies.GetById') + async getById(policyId: string): Promise { + const response = await this.get( + POLICY_ENDPOINTS.GET_BY_ID(policyId) + ); + return response.data; + } + + /** + * Gets the form/configuration data for a policy + * + * @param policyId - The unique identifier of the policy + * @returns Promise resolving to the policy form data + * + * @example + * ```typescript + * const formData = await policies.getFormData('my-policy-id'); + * console.log(formData); + * ``` + */ + @track('Policies.GetFormData') + async getFormData(policyId: string): Promise> { + const response = await this.get>( + POLICY_ENDPOINTS.GET_FORM_DATA(policyId) + ); + return response.data; + } + + /** + * Gets both policy metadata and form data in a single call + * + * @param policyId - The unique identifier of the policy + * @returns Promise resolving to policy details including metadata and form data + * + * @example + * ```typescript + * const details = await policies.getDetails('my-policy-id'); + * console.log(details.policy.name); + * console.log(details.formData); + * ``` + */ + @track('Policies.GetDetails') + async getDetails(policyId: string): Promise { + const [policy, formData] = await Promise.all([ + this.getById(policyId), + this.getFormData(policyId), + ]); + return { policy, formData }; + } + + /** + * Gets all available products + * + * Products define the types of policies that can be created. + * + * @returns Promise resolving to array of products + * + * @example + * ```typescript + * const products = await policies.getProducts(); + * console.log(products.map(p => p.name)); + * ``` + */ + @track('Policies.GetProducts') + async getProducts(): Promise { + const response = await this.get( + PRODUCT_ENDPOINTS.GET_ALL + ); + return response.data; + } + + /** + * Gets the default form template for a product type + * + * Use this to get the initial configuration structure when creating a new policy. + * + * @param productName - The name of the product (e.g., 'AITrustLayer') + * @returns Promise resolving to the form template + * + * @example + * ```typescript + * const template = await policies.getFormTemplate('AITrustLayer'); + * // Use template as initial data for creating a new policy + * ``` + */ + @track('Policies.GetFormTemplate') + async getFormTemplate(productName: string): Promise { + const response = await this.get( + CONTENT_ENDPOINTS.GET_FORM_TEMPLATE(productName) + ); + return response.data; + } + + /** + * Creates a new policy + * + * @param options - Policy creation options + * @returns Promise resolving to the created policy + * + * @example + * ```typescript + * // First get the form template + * const template = await policies.getFormTemplate('AITrustLayer'); + * + * // Create a new policy + * const newPolicy = await policies.create({ + * name: 'My New Policy', + * productName: 'AITrustLayer', + * data: template, + * description: 'Policy description', + * availability: 99 + * }); + * ``` + */ + @track('Policies.Create') + async create(options: PolicyCreateOptions): Promise { + const requestBody = { + policy: { + product: { name: options.productName }, + name: options.name, + description: options.description ?? null, + priority: 0, + availability: options.availability ?? 99, + }, + policyFormData: { + data: { + data: options.data, + }, + }, + }; + + const response = await this.post( + POLICY_ENDPOINTS.SAVE, + requestBody + ); + return response.data; + } + + /** + * Updates an existing policy + * + * @param options - Policy update options including policyId, data, and policy metadata + * @returns Promise resolving when the update is complete + * + * @example + * ```typescript + * // Get current policy details + * const details = await policies.getDetails('my-policy-id'); + * + * // Modify the form data + * const updatedData = { ...details.formData, someSetting: 'newValue' }; + * + * // Update the policy + * await policies.update({ + * policyId: 'my-policy-id', + * data: updatedData, + * policy: { + * product: details.policy.product, + * name: details.policy.name, + * description: details.policy.description, + * priority: details.policy.priority, + * availability: details.policy.availability + * } + * }); + * ``` + */ + @track('Policies.Update') + async update(options: PolicyUpdateOptions): Promise { + const requestBody = { + policy: { + product: options.policy.product, + identifier: options.policyId, + name: options.policy.name, + description: options.policy.description, + priority: options.policy.priority, + availability: options.policy.availability, + }, + policyFormData: { + data: { + data: options.data, + }, + }, + }; + + await this.put(POLICY_ENDPOINTS.SAVE, requestBody); + } + + /** + * Deletes a policy by its identifier + * + * @param policyId - The unique identifier of the policy to delete + * @returns Promise resolving when the deletion is complete + * + * @example + * ```typescript + * await policies.deleteById('my-policy-id'); + * ``` + */ + @track('Policies.Delete') + async deleteById(policyId: string): Promise { + await super.delete(POLICY_ENDPOINTS.DELETE(policyId)); + } +} diff --git a/src/services/aops/tenants.ts b/src/services/aops/tenants.ts new file mode 100644 index 000000000..6955b9d6f --- /dev/null +++ b/src/services/aops/tenants.ts @@ -0,0 +1,173 @@ +import { BaseService } from '../base'; +import { + Tenant, + TenantGetAllOptions, + TenantPolicyAssignment, + TenantPolicyUpdateOptions, +} from '../../models/aops/tenants.types'; +import { + TenantServiceModel, + TenantGetResponse, + TenantPagedResponse, + TenantGetAllRequest, +} from '../../models/aops/tenants.models'; +import { + TENANT_PAGINATION, + TENANT_PAGINATION_PARAMS, +} from '../../models/aops/tenants.constants'; +import { TENANT_ENDPOINTS } from '../../utils/constants/endpoints/aops'; +import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination/types'; +import { PaginationType } from '../../utils/pagination/internal-types'; +import { PaginationHelpers } from '../../utils/pagination/helpers'; +import { track } from '../../core/telemetry'; + +/** + * Service for interacting with UiPath Tenant API + * + * Provides methods for managing tenants and tenant policy assignments + * in the UiPath Autonomous Operations (AoPS) platform. + * + * @example + * ```typescript + * import { UiPath } from '@uipath/uipath-typescript/core'; + * import { Tenants } from '@uipath/uipath-typescript/tenants'; + * + * const sdk = new UiPath(config); + * await sdk.initialize(); + * + * const tenants = new Tenants(sdk); + * + * // Get all tenants + * const allTenants = await tenants.getAll(); + * + * // Get a specific tenant + * const tenant = await tenants.getById('tenant-id'); + * ``` + */ +export class TenantService extends BaseService implements TenantServiceModel { + /** + * Gets all tenants with optional pagination + * + * Note: This API uses PUT method with a request body for pagination. + * + * @param options - Pagination and search options (pageSize, searchTerm) + * @returns Promise resolving to array of tenants or paginated response + * + * @example + * ```typescript + * import { Tenants } from '@uipath/uipath-typescript/tenants'; + * + * const tenants = new Tenants(sdk); + * + * // Get all tenants (non-paginated, default page size 100) + * const allTenants = await tenants.getAll(); + * + * // Get first page with custom page size + * const page1 = await tenants.getAll({ pageSize: 10 }); + * + * // Search for tenants + * const filtered = await tenants.getAll({ searchTerm: 'production' }); + * ``` + */ + @track('Tenants.GetAll') + async getAll( + options?: T + ): Promise< + T extends HasPaginationOptions + ? PaginatedResponse + : NonPaginatedResponse + > { + const opts = options as TenantGetAllOptions | undefined; + const pageSize = opts?.pageSize ?? TENANT_PAGINATION.DEFAULT_PAGE_SIZE; + const searchTerm = opts?.searchTerm ?? ''; + + // Build the request body for the PUT request + const requestBody: TenantGetAllRequest = { + pageIndex: 0, + pageSize: pageSize, + searchTerm: searchTerm, + }; + + // Handle pagination if jumpToPage is specified + if (opts?.jumpToPage && opts.jumpToPage > 1) { + requestBody.pageIndex = opts.jumpToPage - 1; + } + + const response = await this.put( + TENANT_ENDPOINTS.GET_ALL, + requestBody + ); + + // If pagination options are provided, return paginated response + if (opts?.pageSize || opts?.jumpToPage) { + const totalCount = response.data.totalCount; + const currentPage = requestBody.pageIndex + 1; + const hasMore = (currentPage * pageSize) < totalCount; + + return { + data: response.data.result, + hasNextPage: hasMore, + hasPreviousPage: currentPage > 1, + nextCursor: hasMore ? String(currentPage + 1) : undefined, + previousCursor: currentPage > 1 ? String(currentPage - 1) : undefined, + totalCount: totalCount, + } as any; + } + + // Non-paginated response + return { + data: response.data.result, + } as any; + } + + /** + * Gets a tenant by its identifier + * + * @param tenantId - The unique identifier of the tenant + * @returns Promise resolving to the tenant with its policy assignments + * + * @example + * ```typescript + * const tenant = await tenants.getById('my-tenant-id'); + * console.log(tenant.name, tenant.tenantPolicies); + * ``` + */ + @track('Tenants.GetById') + async getById(tenantId: string): Promise { + const response = await this.get( + TENANT_ENDPOINTS.GET_BY_ID(tenantId) + ); + return response.data; + } + + /** + * Updates tenant policies + * + * @param options - Tenant policy update options containing the policies array + * @returns Promise resolving when the update is complete + * + * @example + * ```typescript + * // Get current tenant policies + * const tenant = await tenants.getById('my-tenant-id'); + * + * // Update AITrustLayer policy + * const updatedPolicies = tenant.tenantPolicies.map(policy => { + * if (policy.productIdentifier === 'AITrustLayer') { + * return { ...policy, policyIdentifier: 'new-policy-id' }; + * } + * return policy; + * }); + * + * // Save the updated policies + * await tenants.updatePolicies({ policies: updatedPolicies }); + * ``` + */ + @track('Tenants.UpdatePolicies') + async updatePolicies(options: TenantPolicyUpdateOptions): Promise { + // Remove tenantName from the payload as it's not needed for the POST request + const payload = options.policies.map(({ tenantName, ...policy }) => policy); + + await this.post(TENANT_ENDPOINTS.SAVE, payload); + } +} diff --git a/src/services/index.ts b/src/services/index.ts index c1a875234..28af28ded 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -3,4 +3,5 @@ export * from './data-fabric'; export * from './maestro'; export * from './orchestrator'; export * from './action-center'; +export * from './aops'; diff --git a/src/utils/constants/endpoints/aops.ts b/src/utils/constants/endpoints/aops.ts new file mode 100644 index 000000000..f597ab1ff --- /dev/null +++ b/src/utils/constants/endpoints/aops.ts @@ -0,0 +1,63 @@ +/** + * AoPS (Autonomous Operations) Service Endpoints + */ + +import { AOPS_BASE } from './base'; + +/** + * Policy Service Endpoints + */ +export const POLICY_ENDPOINTS = { + /** Get all policies with pagination */ + GET_ALL: `${AOPS_BASE}/api/Policy/federated/paged`, + + /** Get a single policy by ID */ + GET_BY_ID: (policyId: string) => `${AOPS_BASE}/api/Policy/id/${policyId}`, + + /** Get policy form data by ID */ + GET_FORM_DATA: (policyId: string) => `${AOPS_BASE}/api/Policy/form-data/${policyId}`, + + /** Create or update a policy */ + SAVE: `${AOPS_BASE}/api/Policy`, + + /** Delete a policy by ID */ + DELETE: (policyId: string) => `${AOPS_BASE}/api/Policy/${policyId}`, +} as const; + +/** + * Tenant Service Endpoints + */ +export const TENANT_ENDPOINTS = { + /** Get tenants for the organization (PUT request with pagination) */ + GET_ALL: `${AOPS_BASE}/api/Tenant/`, + + /** Get a specific tenant by ID */ + GET_BY_ID: (tenantId: string) => `${AOPS_BASE}/api/Tenant/${tenantId}`, + + /** Save/update tenant policies */ + SAVE: `${AOPS_BASE}/api/Tenant`, +} as const; + +/** + * Product Service Endpoints + */ +export const PRODUCT_ENDPOINTS = { + /** Get all products */ + GET_ALL: `${AOPS_BASE}/api/Product`, +} as const; + +/** + * Content Service Endpoints + */ +export const CONTENT_ENDPOINTS = { + /** Get form template for a product */ + GET_FORM_TEMPLATE: (productName: string) => `${AOPS_BASE}/api/Content/form-templates/${productName}`, +} as const; + +/** + * License Type Service Endpoints + */ +export const LICENSE_TYPE_ENDPOINTS = { + /** Get all license types */ + GET_ALL: `${AOPS_BASE}/api/LicenseType`, +} as const; diff --git a/src/utils/constants/endpoints/base.ts b/src/utils/constants/endpoints/base.ts index db744dbea..371d8185c 100644 --- a/src/utils/constants/endpoints/base.ts +++ b/src/utils/constants/endpoints/base.ts @@ -7,3 +7,4 @@ export const PIMS_BASE = 'pims_'; export const DATAFABRIC_BASE = 'datafabric_'; export const IDENTITY_BASE = 'identity_'; export const AUTOPILOT_BASE = 'autopilotforeveryone_'; +export const AOPS_BASE = 'roboticsops_'; diff --git a/src/utils/constants/endpoints/index.ts b/src/utils/constants/endpoints/index.ts index f0dd100c2..e89bb8b54 100644 --- a/src/utils/constants/endpoints/index.ts +++ b/src/utils/constants/endpoints/index.ts @@ -20,3 +20,6 @@ export * from './identity'; // Conversational Agent endpoints export * from './conversational-agent'; + +// AoPS (Autonomous Operations) endpoints +export * from './aops';