Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
168 changes: 168 additions & 0 deletions packages/cli/src/actions/deploy-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import chalk from 'chalk';
import ora from 'ora';
import * as fs from 'fs';

Check warning on line 3 in packages/cli/src/actions/deploy-policy.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:fs` over `fs`.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-typescript&issues=AZ0GQXDD4-3ccDloXYGb&open=AZ0GQXDD4-3ccDloXYGb&pullRequest=299
import * as path from 'path';

Check warning on line 4 in packages/cli/src/actions/deploy-policy.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:path` over `path`.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-typescript&issues=AZ0GQXDD4-3ccDloXYGc&open=AZ0GQXDD4-3ccDloXYGc&pullRequest=299
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<TenantResponse> {
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<void> {
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<void> {
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;
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
197 changes: 197 additions & 0 deletions packages/cli/src/actions/publish-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import chalk from 'chalk';
import ora from 'ora';
import * as fs from 'fs';

Check warning on line 3 in packages/cli/src/actions/publish-policy.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:fs` over `fs`.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-typescript&issues=AZ0GQXC64-3ccDloXYGZ&open=AZ0GQXC64-3ccDloXYGZ&pullRequest=299
import * as path from 'path';

Check warning on line 4 in packages/cli/src/actions/publish-policy.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:path` over `path`.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-typescript&issues=AZ0GQXC64-3ccDloXYGa&open=AZ0GQXC64-3ccDloXYGa&pullRequest=299
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<string, unknown>;
}

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<string, unknown> {
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<string, unknown>,
envConfig: EnvironmentConfig
): Promise<PolicyCreateResponse> {
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),

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
});

if (!response.ok) {
await handleHttpError(response, MESSAGES.ERROR_CONTEXT.POLICY_PUBLISHING);
}

return (await response.json()) as PolicyCreateResponse;
}

export async function executePublishPolicy(options: PublishPolicyOptions): Promise<void> {
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;
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/auth/utils/url.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down
Loading
Loading