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
2 changes: 2 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
echo "traces_test_trace_id=${{ secrets.UIPATH_TRACES_TEST_TRACE_ID_DEV || secrets.UIPATH_TRACES_TEST_TRACE_ID }}" >> $GITHUB_OUTPUT
echo "tasks_test_user_group_id=${{ secrets.UIPATH_TASKS_TEST_USER_GROUP_ID_DEV || secrets.UIPATH_TASKS_TEST_USER_GROUP_ID }}" >> $GITHUB_OUTPUT
echo "tasks_test_user_id=${{ secrets.UIPATH_TASKS_TEST_USER_ID_DEV || secrets.UIPATH_TASKS_TEST_USER_ID }}" >> $GITHUB_OUTPUT
echo "identity_test_user_id=${{ secrets.UIPATH_IDENTITY_TEST_USER_ID_DEV || secrets.UIPATH_IDENTITY_TEST_USER_ID }}" >> $GITHUB_OUTPUT

- name: Create Integration Test Configuration
run: |
Expand Down Expand Up @@ -105,6 +106,7 @@ jobs:
TRACES_TEST_TRACE_ID=${{ steps.config.outputs.traces_test_trace_id }}
TASKS_TEST_USER_GROUP_ID=${{ steps.config.outputs.tasks_test_user_group_id }}
TASKS_TEST_USER_ID=${{ steps.config.outputs.tasks_test_user_id }}
IDENTITY_TEST_USER_ID=${{ steps.config.outputs.identity_test_user_id }}
EOF

- name: Run Integration Tests
Expand Down
8 changes: 8 additions & 0 deletions docs/oauth-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,11 @@ The `ConversationalAgents` scope is required for real-time WebSocket sessions (`
| `getSpansByReference()` | `Insights.RealTimeData Insights OR.Folders.Read` |
| `getGovernanceDecisions()` | `Insights.RealTimeData Insights OR.Folders.Read` |
| `getGovernanceSummary()` | `Insights.RealTimeData Insights OR.Folders.Read` |

## Users

User management is authorized by the caller's **organization role** (organization administrator), not by OAuth scopes — these endpoints enforce no scope requirement. Any token belonging to an org administrator can call them.

| Method | OAuth Scope |
|--------|-------------|
| `getById()` | None — requires organization administrator role |
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ nav:
- Processes: api/interfaces/ProcessServiceModel.md
- Queues: api/interfaces/QueueServiceModel.md
- Tasks: api/interfaces/TaskServiceModel.md
- Users: api/interfaces/UserServiceModel.md
- Traces:
- api/interfaces/traces/index.md
- Agent: api/interfaces/AgentTracesServiceModel.md
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@
"types": "./dist/notifications/index.d.ts",
"default": "./dist/notifications/index.cjs"
}
},
"./users": {
"import": {
"types": "./dist/users/index.d.ts",
"default": "./dist/users/index.mjs"
},
"require": {
"types": "./dist/users/index.d.ts",
"default": "./dist/users/index.cjs"
}
}
},
"files": [
Expand Down
5 changes: 5 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ const serviceEntries = [
name: 'notifications',
input: 'src/services/notification/index.ts',
output: 'notifications/index'
},
{
name: 'users',
input: 'src/services/identity/index.ts',
output: 'users/index'
}
];

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './models/conversational-agent';
export * from './models/agents';
export * as DuFramework from './models/document-understanding/framework';
export * from './models/governance';
export * from './models/identity';
export * from './models/observability';

// Export error handling functionality (public API only)
Expand Down
6 changes: 6 additions & 0 deletions src/models/identity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Identity models barrel export.
*/

export * from './users.types';
export * from './users.models';
34 changes: 34 additions & 0 deletions src/models/identity/users.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { UserType, UserCategory } from './users.types';

/**
* User field mappings (API field name → SDK field name).
*
* Semantic renames only — the API already returns camelCase, so no case
* conversion is involved.
*/
export const UserMap: { [key: string]: string } = {
creationTime: 'createdTime',
lastModificationTime: 'lastModifiedTime',
groupIDs: 'groupIds',
};

/**
* Maps numeric user type codes (from API) to {@link UserType} enum values.
*/
export const UserTypeMap: { [key: number]: UserType } = {
0: UserType.User,
1: UserType.Robot,
2: UserType.DirectoryUser,
3: UserType.DirectoryGroup,
4: UserType.RobotAccount,
5: UserType.Application,
};

/**
* Maps numeric user category codes (from API) to {@link UserCategory} enum values.
*/
export const UserCategoryMap: { [key: number]: UserCategory } = {
0: UserCategory.Local,
1: UserCategory.LinkedLocal,
2: UserCategory.Directory,
};
48 changes: 48 additions & 0 deletions src/models/identity/users.internal-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Internal-only types for the Identity Users service.
*
* NOT exported from the public barrel (`src/models/identity/index.ts`).
*/

/**
* Raw user shape as returned by the Identity user management API.
*
* Mirrors the API contract exactly: it uses the API's field names
* (`creationTime`, `groupIDs`, …) and numeric `type`/`category` codes — the
* Swagger spec declares string enums for these, but the live API returns
* numbers. The SDK renames/maps them before returning to consumers.
*/
export interface RawUserEntry {
id: string;
userName: string;
email: string;
emailConfirmed: boolean;
name: string | null;
surname: string | null;
displayName: string | null;
/** Renamed to `createdTime` in the SDK response. */
creationTime: string;
/** Renamed to `lastModifiedTime` in the SDK response. */
lastModificationTime: string | null;
lastLoginTime: string | null;
/** Renamed to `groupIds` in the SDK response. */
groupIDs: string[];
isActive: boolean;
bypassBasicAuthRestriction: boolean;
/** Numeric code mapped to the {@link UserType} enum in the SDK response. */
type: number;
/** Numeric code mapped to the {@link UserCategory} enum in the SDK response. */
category: number;
invitationAccepted: boolean;
// Internal field the SDK strips before returning to consumers:
/** Internal platform synchronization id — dropped from the SDK response. */
legacyId?: number;
}

/**
* Fields stripped from each {@link RawUserEntry} before it is returned to the
* SDK consumer.
*/
export const INTERNAL_USER_FIELDS = [
'legacyId',
] as const satisfies ReadonlyArray<keyof RawUserEntry>;
46 changes: 46 additions & 0 deletions src/models/identity/users.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Identity Users service model — public response shapes and the ServiceModel
* interface that drives generated API documentation.
*/

import type { RawUserGetResponse } from './users.types';

/**
* User returned by the Users service.
*/
export interface UserGetResponse extends RawUserGetResponse {}

/**
* Service model for managing users in a UiPath organization.
*
* Provides organization-level user administration.
*
* ### Usage
*
* Prerequisites: Initialize the SDK first - see [Getting Started](/uipath-typescript/getting-started/#import-initialize)
*
* ```typescript
* import { Users } from '@uipath/uipath-typescript/users';
*
* const users = new Users(sdk);
* const user = await users.getById('<userId>');
* ```
*/
export interface UserServiceModel {
/**
* Gets a user by ID.
*
* Returns the full user details including profile fields, group membership,
* activity flags and invitation state.
*
* @param userId - User GUID
* @returns Promise resolving to a {@link UserGetResponse} with the user's profile fields, group membership, activity flags and invitation state
*
* @example
* ```typescript
* const user = await users.getById('<userId>');
* console.log(`${user.displayName} (${user.email}) — active: ${user.isActive}`);
* ```
*/
getById(userId: string): Promise<UserGetResponse>;
}
74 changes: 74 additions & 0 deletions src/models/identity/users.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Identity user management types — response shapes and enums.
*/

/**
* Defines how a user was created and how it is supposed to be used.
*/
export enum UserType {
/** Standard interactive user. */
User = 'user',
/** Robot user driving unattended automation. */
Robot = 'robot',
/** User provisioned from an external directory (e.g. Azure AD). */
DirectoryUser = 'directoryUser',
/** Group provisioned from an external directory. */
DirectoryGroup = 'directoryGroup',
/** Robot account (non-interactive machine identity). */
RobotAccount = 'robotAccount',
/** External application identity. */
Application = 'application',
}

/**
* Discriminates a user by how they relate to directory provisioning.
*/
export enum UserCategory {
/** Local account managed in UiPath. */
Local = 'local',
/** Local account linked to a directory identity. */
LinkedLocal = 'linkedLocal',
/** Account provisioned from an external directory. */
Directory = 'directory',
}

/**
* User as returned by the Identity user management API.
*
* Field selection: `legacyId` (an internal platform synchronization field) is returned
* by the API but dropped from the SDK because it has no use for an application developer.
*/
export interface RawUserGetResponse {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc says "User as returned by the Identity user management API" but RawUserGetResponse is the SDK-facing shape (after field renames and numeric→enum mapping), not the raw API wire format. The actual API response shape is RawUserEntry in users.internal-types.ts — which has creationTime, groupIDs, and numeric type/category codes.

Per CLAUDE.md: Raw{Entity}GetResponse types describe the shape before method attachment, not the literal API wire format. A contributor implementing a future method (e.g. getAll) who reads this comment could incorrectly assume the API returns createdTime / UserType values and skip the transform step.

Suggested change
export interface RawUserGetResponse {
/**
* SDK-facing user shape returned by {@link UserServiceModel.getById}.
*
* Field selection: `legacyId` (an internal platform synchronization field) is returned
* by the API but dropped from the SDK because it has no use for an application developer.
*/
export interface RawUserGetResponse {

/** User GUID. */
id: string;
/** The username. */
userName: string;
/** Email address. Empty string when the user has no email. */
email: string;
/** Whether the email address has been confirmed. */
emailConfirmed: boolean;
/** First name. */
name: string | null;
/** Last name. */
surname: string | null;
/** Display name. */
displayName: string | null;
/** When the user was created. */
createdTime: string;
/** When the user was last modified. */
lastModifiedTime: string | null;
/** When the user last logged in. `null` if the user has never logged in. */
lastLoginTime: string | null;
/** GUIDs of the groups the user is a member of. */
groupIds: string[];
/** Whether the user is active. */
isActive: boolean;
/** Whether this user bypasses the basic authentication restriction. */
bypassBasicAuthRestriction: boolean;
/** How the user was created and is supposed to be used. */
type: UserType;
/** How the user relates to directory provisioning. */
category: UserCategory;
/** Whether the user has accepted their invitation. */
invitationAccepted: boolean;
}
25 changes: 25 additions & 0 deletions src/services/identity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Identity Users Service Module
*
* Provides organization-level user administration via the UiPath Identity API:
* - `Users` — retrieve users in the organization
*
* @example
* ```typescript
* import { UiPath } from '@uipath/uipath-typescript/core';
* import { Users } from '@uipath/uipath-typescript/users';
*
* const sdk = new UiPath(config);
* await sdk.initialize();
*
* const users = new Users(sdk);
* const user = await users.getById('<userId>');
* ```
*
* @module
*/

export { UserService as Users } from './users';

// Models (types, enums, response shapes)
export * from '../../models/identity';
49 changes: 49 additions & 0 deletions src/services/identity/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* UserService — organization-level user administration via the Identity API.
*/

import { track } from '../../core/telemetry';
import { BaseService } from '../base';

import type { RawUserGetResponse } from '../../models/identity/users.types';
import type { UserGetResponse, UserServiceModel } from '../../models/identity/users.models';
import {
INTERNAL_USER_FIELDS,
type RawUserEntry,
} from '../../models/identity/users.internal-types';
import { UserCategoryMap, UserMap, UserTypeMap } from '../../models/identity/users.constants';

import { IDENTITY_USER_ENDPOINTS } from '../../utils/constants/endpoints';
import { applyDataTransforms, transformData } from '../../utils/transform';

/**
* Service for managing users in a UiPath organization.
*
* All operations route at the **organization** level — no tenant or folder
* context is involved.
*/
export class UserService extends BaseService implements UserServiceModel {
@track('Users.GetById')
async getById(userId: string): Promise<UserGetResponse> {
const response = await this.get<RawUserEntry>(IDENTITY_USER_ENDPOINTS.BY_ID(userId));
return this.transformUser(response.data);
}

/**
* Strips internal fields from a raw user entry and applies the SDK field
* renames and numeric → enum value mappings before returning it to the consumer.
*/
private transformUser(user: RawUserEntry): RawUserGetResponse {
const stripped: RawUserEntry = { ...user };
for (const field of INTERNAL_USER_FIELDS) {
delete stripped[field];
}
const renamed = transformData(stripped, UserMap) as unknown as RawUserGetResponse;
return applyDataTransforms(renamed, {
field: 'type',
valueMap: UserTypeMap,
transform: (data) =>
applyDataTransforms(data, { field: 'category', valueMap: UserCategoryMap }),
});
Comment on lines +42 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-standard use of the transform callback to chain a second applyDataTransforms call. The transform option is designed for arbitrary post-processing, not for chaining field-value maps; using it this way is unusual and makes extending the pipeline (e.g., a third enum field) require further nesting.

The conventional pattern throughout the SDK is two sequential calls:

Suggested change
return applyDataTransforms(renamed, {
field: 'type',
valueMap: UserTypeMap,
transform: (data) =>
applyDataTransforms(data, { field: 'category', valueMap: UserCategoryMap }),
});
const withType = applyDataTransforms(renamed, { field: 'type', valueMap: UserTypeMap });
return applyDataTransforms(withType, { field: 'category', valueMap: UserCategoryMap });

}
}
11 changes: 11 additions & 0 deletions src/utils/constants/endpoints/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
*/

export const ORCHESTRATOR_BASE = 'orchestrator_';
/**
* Identity server base for **organization**-level API routes (e.g. user management).
* Identity URLs do not include a tenant segment, so — like {@link NOTIFICATION_BASE} —
* the `../` prefix relies on `URL` path normalization to collapse the tenant segment
* that {@link ApiClient} unconditionally inserts (`{orgName}/{tenantName}/{path}`):
* `{orgName}/{tenantName}/../identity_/...` resolves to `{orgName}/identity_/...`.
*
* Do NOT remove the leading `../`. OAuth/token endpoints use {@link IDENTITY_BASE}
* instead because they are resolved outside {@link ApiClient}.
*/
export const IDENTITY_ORG_BASE = '../identity_';
export const PIMS_BASE = 'pims_';
export const DATAFABRIC_BASE = 'datafabric_';
export const IDENTITY_BASE = 'identity_';
Expand Down
Loading
Loading