diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index f09bad7100d..94d882a86e8 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -102,7 +102,7 @@ async function handleLocalFile(filename: string, userId: string): Promise { try { - const filePath = findLocalFile(filename) + const filePath = await findLocalFile(filename) if (!filePath) { throw new FileNotFoundError(`File not found: ${filename}`) diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index e97aeeb3707..48f85bf21ba 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -75,7 +75,7 @@ export async function POST(request: NextRequest) { const uploadResults = [] for (const file of files) { - const originalName = file.name || 'untitled' + const originalName = file.name || 'untitled.md' if (!validateFileExtension(originalName)) { const extension = originalName.split('.').pop()?.toLowerCase() || 'unknown' diff --git a/apps/sim/app/api/files/utils.test.ts b/apps/sim/app/api/files/utils.test.ts index 58d1791f922..ff02212a808 100644 --- a/apps/sim/app/api/files/utils.test.ts +++ b/apps/sim/app/api/files/utils.test.ts @@ -331,7 +331,7 @@ describe('extractFilename', () => { describe('findLocalFile - Path Traversal Security Tests', () => { describe('path traversal attack prevention', () => { - it.concurrent('should reject classic path traversal attacks', () => { + it.concurrent('should reject classic path traversal attacks', async () => { const maliciousInputs = [ '../../../etc/passwd', '..\\..\\..\\windows\\system32\\config\\sam', @@ -340,35 +340,35 @@ describe('findLocalFile - Path Traversal Security Tests', () => { '..\\config.ini', ] - maliciousInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of maliciousInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject encoded path traversal attempts', () => { + it.concurrent('should reject encoded path traversal attempts', async () => { const encodedInputs = [ '%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64', // ../../../etc/passwd '..%2f..%2fetc%2fpasswd', '..%5c..%5cconfig.ini', ] - encodedInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of encodedInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject mixed path separators', () => { + it.concurrent('should reject mixed path separators', async () => { const mixedInputs = ['../..\\config.txt', '..\\../secret.ini', '/..\\..\\system32'] - mixedInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of mixedInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject filenames with dangerous characters', () => { + it.concurrent('should reject filenames with dangerous characters', async () => { const dangerousInputs = [ 'file:with:colons.txt', 'file|with|pipes.txt', @@ -376,43 +376,45 @@ describe('findLocalFile - Path Traversal Security Tests', () => { 'file*with*asterisks.txt', ] - dangerousInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of dangerousInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject null and empty inputs', () => { - expect(findLocalFile('')).toBeNull() - expect(findLocalFile(' ')).toBeNull() - expect(findLocalFile('\t\n')).toBeNull() + it.concurrent('should reject null and empty inputs', async () => { + expect(await findLocalFile('')).toBeNull() + expect(await findLocalFile(' ')).toBeNull() + expect(await findLocalFile('\t\n')).toBeNull() }) - it.concurrent('should reject filenames that become empty after sanitization', () => { + it.concurrent('should reject filenames that become empty after sanitization', async () => { const emptyAfterSanitization = ['../..', '..\\..\\', '////', '....', '..'] - emptyAfterSanitization.forEach((input) => { - const result = findLocalFile(input) + for (const input of emptyAfterSanitization) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) }) describe('security validation passes for legitimate files', () => { - it.concurrent('should accept properly formatted filenames without throwing errors', () => { - const legitimateInputs = [ - 'document.pdf', - 'image.png', - 'data.csv', - 'report-2024.doc', - 'file_with_underscores.txt', - 'file-with-dashes.json', - ] - - legitimateInputs.forEach((input) => { - // Should not throw security errors for legitimate filenames - expect(() => findLocalFile(input)).not.toThrow() - }) - }) + it.concurrent( + 'should accept properly formatted filenames without throwing errors', + async () => { + const legitimateInputs = [ + 'document.pdf', + 'image.png', + 'data.csv', + 'report-2024.doc', + 'file_with_underscores.txt', + 'file-with-dashes.json', + ] + + for (const input of legitimateInputs) { + await expect(findLocalFile(input)).resolves.toBeDefined() + } + } + ) }) }) diff --git a/apps/sim/app/api/files/utils.ts b/apps/sim/app/api/files/utils.ts index a4831cdd3b7..d290288f2da 100644 --- a/apps/sim/app/api/files/utils.ts +++ b/apps/sim/app/api/files/utils.ts @@ -1,8 +1,5 @@ -import { existsSync } from 'fs' -import path from 'path' import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' -import { UPLOAD_DIR } from '@/lib/uploads/config' import { sanitizeFileKey } from '@/lib/uploads/utils/file-utils' const logger = createLogger('FilesUtils') @@ -123,76 +120,29 @@ export function extractFilename(path: string): string { return filename } -function sanitizeFilename(filename: string): string { - if (!filename || typeof filename !== 'string') { - throw new Error('Invalid filename provided') - } - - if (!filename.includes('/')) { - throw new Error('File key must include a context prefix (e.g., kb/, workspace/, execution/)') - } - - const segments = filename.split('/') - - const sanitizedSegments = segments.map((segment) => { - if (segment === '..' || segment === '.') { - throw new Error('Path traversal detected') - } - - const sanitized = segment.replace(/\.\./g, '').replace(/[\\]/g, '').replace(/^\./g, '').trim() - - if (!sanitized) { - throw new Error('Invalid or empty path segment after sanitization') - } - - if ( - sanitized.includes(':') || - sanitized.includes('|') || - sanitized.includes('?') || - sanitized.includes('*') || - sanitized.includes('\x00') || - /[\x00-\x1F\x7F]/.test(sanitized) - ) { - throw new Error('Path segment contains invalid characters') - } - - return sanitized - }) - - return sanitizedSegments.join(path.sep) -} - -export function findLocalFile(filename: string): string | null { +export async function findLocalFile(filename: string): Promise { try { const sanitizedFilename = sanitizeFileKey(filename) - // Reject if sanitized filename is empty or only contains path separators/dots if (!sanitizedFilename || !sanitizedFilename.trim() || /^[/\\.\s]+$/.test(sanitizedFilename)) { return null } - const possiblePaths = [ - path.join(UPLOAD_DIR, sanitizedFilename), - path.join(process.cwd(), 'uploads', sanitizedFilename), - ] + const { existsSync } = await import('fs') + const path = await import('path') + const { UPLOAD_DIR_SERVER } = await import('@/lib/uploads/core/setup.server') - for (const filePath of possiblePaths) { - const resolvedPath = path.resolve(filePath) - const allowedDirs = [path.resolve(UPLOAD_DIR), path.resolve(process.cwd(), 'uploads')] + const resolvedPath = path.join(UPLOAD_DIR_SERVER, sanitizedFilename) - // Must be within allowed directory but NOT the directory itself - const isWithinAllowedDir = allowedDirs.some( - (allowedDir) => - resolvedPath.startsWith(allowedDir + path.sep) && resolvedPath !== allowedDir - ) - - if (!isWithinAllowedDir) { - continue - } + if ( + !resolvedPath.startsWith(UPLOAD_DIR_SERVER + path.sep) || + resolvedPath === UPLOAD_DIR_SERVER + ) { + return null + } - if (existsSync(resolvedPath)) { - return resolvedPath - } + if (existsSync(resolvedPath)) { + return resolvedPath } return null diff --git a/apps/sim/app/api/tools/workday/assign-onboarding/route.ts b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts new file mode 100644 index 00000000000..c04e1c65db5 --- /dev/null +++ b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts @@ -0,0 +1,67 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayAssignOnboardingAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + onboardingPlanId: z.string().min(1), + actionEventId: z.string().min(1), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Put_Onboarding_Plan_AssignmentAsync({ + Onboarding_Plan_Assignment_Data: { + Onboarding_Plan_Reference: wdRef('Onboarding_Plan_ID', data.onboardingPlanId), + Person_Reference: wdRef('WID', data.workerId), + Action_Event_Reference: wdRef('Background_Check_ID', data.actionEventId), + Assignment_Effective_Moment: new Date().toISOString(), + Active: true, + }, + }) + + return NextResponse.json({ + success: true, + output: { + assignmentId: extractRefId(result?.Onboarding_Plan_Assignment_Reference), + workerId: data.workerId, + planId: data.onboardingPlanId, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday assign onboarding failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/change-job/route.ts b/apps/sim/app/api/tools/workday/change-job/route.ts new file mode 100644 index 00000000000..6858a49a649 --- /dev/null +++ b/apps/sim/app/api/tools/workday/change-job/route.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayChangeJobAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + effectiveDate: z.string().min(1), + newPositionId: z.string().optional(), + newJobProfileId: z.string().optional(), + newLocationId: z.string().optional(), + newSupervisoryOrgId: z.string().optional(), + reason: z.string().min(1, 'Reason is required for job changes'), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const changeJobDetailData: Record = { + Reason_Reference: wdRef('Change_Job_Subcategory_ID', data.reason), + } + if (data.newPositionId) { + changeJobDetailData.Position_Reference = wdRef('Position_ID', data.newPositionId) + } + if (data.newJobProfileId) { + changeJobDetailData.Job_Profile_Reference = wdRef('Job_Profile_ID', data.newJobProfileId) + } + if (data.newLocationId) { + changeJobDetailData.Location_Reference = wdRef('Location_ID', data.newLocationId) + } + if (data.newSupervisoryOrgId) { + changeJobDetailData.Supervisory_Organization_Reference = wdRef( + 'Supervisory_Organization_ID', + data.newSupervisoryOrgId + ) + } + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + const [result] = await client.Change_JobAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Change_Job_Data: { + Worker_Reference: wdRef('Employee_ID', data.workerId), + Effective_Date: data.effectiveDate, + Change_Job_Detail_Data: changeJobDetailData, + }, + }) + + const eventRef = result?.Event_Reference + + return NextResponse.json({ + success: true, + output: { + eventId: extractRefId(eventRef), + workerId: data.workerId, + effectiveDate: data.effectiveDate, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday change job failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/create-prehire/route.ts b/apps/sim/app/api/tools/workday/create-prehire/route.ts new file mode 100644 index 00000000000..d9a955b4187 --- /dev/null +++ b/apps/sim/app/api/tools/workday/create-prehire/route.ts @@ -0,0 +1,134 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayCreatePrehireAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + legalName: z.string().min(1), + email: z.string().optional(), + phoneNumber: z.string().optional(), + address: z.string().optional(), + countryCode: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + if (!data.email && !data.phoneNumber && !data.address) { + return NextResponse.json( + { + success: false, + error: 'At least one contact method (email, phone, or address) is required', + }, + { status: 400 } + ) + } + + const parts = data.legalName.trim().split(/\s+/) + const firstName = parts[0] ?? '' + const lastName = parts.length > 1 ? parts.slice(1).join(' ') : '' + + if (!lastName) { + return NextResponse.json( + { success: false, error: 'Legal name must include both a first name and last name' }, + { status: 400 } + ) + } + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + const contactData: Record = {} + if (data.email) { + contactData.Email_Address_Data = [ + { + Email_Address: data.email, + Usage_Data: { + Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') }, + Public: true, + }, + }, + ] + } + if (data.phoneNumber) { + contactData.Phone_Data = [ + { + Phone_Number: data.phoneNumber, + Phone_Device_Type_Reference: wdRef('Phone_Device_Type_ID', 'Landline'), + Usage_Data: { + Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') }, + Public: true, + }, + }, + ] + } + if (data.address) { + contactData.Address_Data = [ + { + Formatted_Address: data.address, + Usage_Data: { + Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') }, + Public: true, + }, + }, + ] + } + + const [result] = await client.Put_ApplicantAsync({ + Applicant_Data: { + Personal_Data: { + Name_Data: { + Legal_Name_Data: { + Name_Detail_Data: { + Country_Reference: wdRef('ISO_3166-1_Alpha-2_Code', data.countryCode ?? 'US'), + First_Name: firstName, + Last_Name: lastName, + }, + }, + }, + Contact_Information_Data: contactData, + }, + }, + }) + + const applicantRef = result?.Applicant_Reference + + return NextResponse.json({ + success: true, + output: { + preHireId: extractRefId(applicantRef), + descriptor: applicantRef?.attributes?.Descriptor ?? null, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday create prehire failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/get-compensation/route.ts b/apps/sim/app/api/tools/workday/get-compensation/route.ts new file mode 100644 index 00000000000..a78a1619933 --- /dev/null +++ b/apps/sim/app/api/tools/workday/get-compensation/route.ts @@ -0,0 +1,101 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayCompensationDataSoap, + type WorkdayCompensationPlanSoap, + type WorkdayWorkerSoap, +} from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayGetCompensationAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Get_WorkersAsync({ + Request_References: { + Worker_Reference: { + ID: { attributes: { 'wd:type': 'Employee_ID' }, $value: data.workerId }, + }, + }, + Response_Group: { + Include_Reference: true, + Include_Compensation: true, + }, + }) + + const worker = + normalizeSoapArray( + result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined + )[0] ?? null + const compensationData = worker?.Worker_Data?.Compensation_Data + + const mapPlan = (p: WorkdayCompensationPlanSoap) => ({ + id: extractRefId(p.Compensation_Plan_Reference) ?? null, + planName: p.Compensation_Plan_Reference?.attributes?.Descriptor ?? null, + amount: p.Amount ?? p.Per_Unit_Amount ?? p.Individual_Target_Amount ?? null, + currency: extractRefId(p.Currency_Reference) ?? null, + frequency: extractRefId(p.Frequency_Reference) ?? null, + }) + + const planTypeKeys: (keyof WorkdayCompensationDataSoap)[] = [ + 'Employee_Base_Pay_Plan_Assignment_Data', + 'Employee_Salary_Unit_Plan_Assignment_Data', + 'Employee_Bonus_Plan_Assignment_Data', + 'Employee_Allowance_Plan_Assignment_Data', + 'Employee_Commission_Plan_Assignment_Data', + 'Employee_Stock_Plan_Assignment_Data', + 'Employee_Period_Salary_Plan_Assignment_Data', + ] + + const compensationPlans: ReturnType[] = [] + for (const key of planTypeKeys) { + for (const plan of normalizeSoapArray(compensationData?.[key])) { + compensationPlans.push(mapPlan(plan)) + } + } + + return NextResponse.json({ + success: true, + output: { compensationPlans }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday get compensation failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/get-organizations/route.ts b/apps/sim/app/api/tools/workday/get-organizations/route.ts new file mode 100644 index 00000000000..93adddd0b86 --- /dev/null +++ b/apps/sim/app/api/tools/workday/get-organizations/route.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayOrganizationSoap, +} from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayGetOrganizationsAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + type: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const limit = data.limit ?? 20 + const offset = data.offset ?? 0 + const page = offset > 0 ? Math.floor(offset / limit) + 1 : 1 + + const [result] = await client.Get_OrganizationsAsync({ + Response_Filter: { Page: page, Count: limit }, + Request_Criteria: data.type + ? { + Organization_Type_Reference: { + ID: { + attributes: { 'wd:type': 'Organization_Type_ID' }, + $value: data.type, + }, + }, + } + : undefined, + Response_Group: { Include_Hierarchy_Data: true }, + }) + + const orgsArray = normalizeSoapArray( + result?.Response_Data?.Organization as + | WorkdayOrganizationSoap + | WorkdayOrganizationSoap[] + | undefined + ) + + const organizations = orgsArray.map((o) => ({ + id: extractRefId(o.Organization_Reference) ?? null, + descriptor: o.Organization_Descriptor ?? null, + type: extractRefId(o.Organization_Data?.Organization_Type_Reference) ?? null, + subtype: extractRefId(o.Organization_Data?.Organization_Subtype_Reference) ?? null, + isActive: o.Organization_Data?.Inactive != null ? !o.Organization_Data.Inactive : null, + })) + + const total = result?.Response_Results?.Total_Results ?? organizations.length + + return NextResponse.json({ + success: true, + output: { organizations, total }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday get organizations failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/get-worker/route.ts b/apps/sim/app/api/tools/workday/get-worker/route.ts new file mode 100644 index 00000000000..904c5cf4132 --- /dev/null +++ b/apps/sim/app/api/tools/workday/get-worker/route.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayWorkerSoap, +} from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayGetWorkerAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Get_WorkersAsync({ + Request_References: { + Worker_Reference: { + ID: { attributes: { 'wd:type': 'Employee_ID' }, $value: data.workerId }, + }, + }, + Response_Group: { + Include_Reference: true, + Include_Personal_Information: true, + Include_Employment_Information: true, + Include_Compensation: true, + Include_Organizations: true, + }, + }) + + const worker = + normalizeSoapArray( + result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined + )[0] ?? null + + return NextResponse.json({ + success: true, + output: { + worker: worker + ? { + id: extractRefId(worker.Worker_Reference) ?? null, + descriptor: worker.Worker_Descriptor ?? null, + personalData: worker.Worker_Data?.Personal_Data ?? null, + employmentData: worker.Worker_Data?.Employment_Data ?? null, + compensationData: worker.Worker_Data?.Compensation_Data ?? null, + organizationData: worker.Worker_Data?.Organization_Data ?? null, + } + : null, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday get worker failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/hire/route.ts b/apps/sim/app/api/tools/workday/hire/route.ts new file mode 100644 index 00000000000..1c6c8abc8b8 --- /dev/null +++ b/apps/sim/app/api/tools/workday/hire/route.ts @@ -0,0 +1,78 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayHireAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + preHireId: z.string().min(1), + positionId: z.string().min(1), + hireDate: z.string().min(1), + employeeType: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + const [result] = await client.Hire_EmployeeAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Hire_Employee_Data: { + Applicant_Reference: wdRef('Applicant_ID', data.preHireId), + Position_Reference: wdRef('Position_ID', data.positionId), + Hire_Date: data.hireDate, + Hire_Employee_Event_Data: { + Employee_Type_Reference: wdRef('Employee_Type_ID', data.employeeType ?? 'Regular'), + First_Day_of_Work: data.hireDate, + }, + }, + }) + + const employeeRef = result?.Employee_Reference + const eventRef = result?.Event_Reference + + return NextResponse.json({ + success: true, + output: { + workerId: extractRefId(employeeRef), + employeeId: extractRefId(employeeRef), + eventId: extractRefId(eventRef), + hireDate: data.hireDate, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday hire employee failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/list-workers/route.ts b/apps/sim/app/api/tools/workday/list-workers/route.ts new file mode 100644 index 00000000000..e8f31950367 --- /dev/null +++ b/apps/sim/app/api/tools/workday/list-workers/route.ts @@ -0,0 +1,83 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayWorkerSoap, +} from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayListWorkersAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + limit: z.number().optional(), + offset: z.number().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const limit = data.limit ?? 20 + const offset = data.offset ?? 0 + const page = offset > 0 ? Math.floor(offset / limit) + 1 : 1 + + const [result] = await client.Get_WorkersAsync({ + Response_Filter: { Page: page, Count: limit }, + Response_Group: { + Include_Reference: true, + Include_Personal_Information: true, + Include_Employment_Information: true, + }, + }) + + const workersArray = normalizeSoapArray( + result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined + ) + + const workers = workersArray.map((w) => ({ + id: extractRefId(w.Worker_Reference) ?? null, + descriptor: w.Worker_Descriptor ?? null, + personalData: w.Worker_Data?.Personal_Data ?? null, + employmentData: w.Worker_Data?.Employment_Data ?? null, + })) + + const total = result?.Response_Results?.Total_Results ?? workers.length + + return NextResponse.json({ + success: true, + output: { workers, total }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday list workers failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/terminate/route.ts b/apps/sim/app/api/tools/workday/terminate/route.ts new file mode 100644 index 00000000000..8484d781a03 --- /dev/null +++ b/apps/sim/app/api/tools/workday/terminate/route.ts @@ -0,0 +1,77 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayTerminateAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + terminationDate: z.string().min(1), + reason: z.string().min(1), + notificationDate: z.string().optional(), + lastDayOfWork: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + const [result] = await client.Terminate_EmployeeAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Terminate_Employee_Data: { + Employee_Reference: wdRef('Employee_ID', data.workerId), + Termination_Date: data.terminationDate, + Terminate_Event_Data: { + Primary_Reason_Reference: wdRef('Termination_Subcategory_ID', data.reason), + Last_Day_of_Work: data.lastDayOfWork ?? data.terminationDate, + Notification_Date: data.notificationDate ?? data.terminationDate, + }, + }, + }) + + const eventRef = result?.Event_Reference + + return NextResponse.json({ + success: true, + output: { + eventId: extractRefId(eventRef), + workerId: data.workerId, + terminationDate: data.terminationDate, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday terminate employee failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/update-worker/route.ts b/apps/sim/app/api/tools/workday/update-worker/route.ts new file mode 100644 index 00000000000..dbf2f1c5799 --- /dev/null +++ b/apps/sim/app/api/tools/workday/update-worker/route.ts @@ -0,0 +1,66 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayUpdateWorkerAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + fields: z.record(z.unknown()), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Change_Personal_InformationAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Change_Personal_Information_Data: { + Person_Reference: wdRef('Employee_ID', data.workerId), + Personal_Information_Data: data.fields, + }, + }) + + return NextResponse.json({ + success: true, + output: { + eventId: extractRefId(result?.Personal_Information_Change_Event_Reference), + workerId: data.workerId, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday update worker failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts index b452ae47006..18f4faa2afd 100644 --- a/apps/sim/app/api/workspaces/[id]/files/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/route.ts @@ -93,7 +93,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'No file provided' }, { status: 400 }) } - const fileName = rawFile.name || 'untitled' + const fileName = rawFile.name || 'untitled.md' const maxSize = 100 * 1024 * 1024 if (rawFile.size > maxSize) { diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index ac156edf562..448a1b251d6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -151,6 +151,8 @@ export function Files() { } const justCreatedFileIdRef = useRef(null) + const filesRef = useRef(files) + filesRef.current = files const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 }) @@ -483,11 +485,11 @@ export function Files() { if (isJustCreated) { setPreviewMode('editor') } else { - const file = selectedFileId ? files.find((f) => f.id === selectedFileId) : null + const file = selectedFileId ? filesRef.current.find((f) => f.id === selectedFileId) : null const canPreview = file ? isPreviewable(file) : false setPreviewMode(canPreview ? 'preview' : 'editor') } - }, [selectedFileId, files]) + }, [selectedFileId]) useEffect(() => { if (!selectedFile) return diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts new file mode 100644 index 00000000000..b55e4e7fc0f --- /dev/null +++ b/apps/sim/blocks/blocks/workday.ts @@ -0,0 +1,440 @@ +import { WorkdayIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const WorkdayBlock: BlockConfig = { + type: 'workday', + name: 'Workday', + description: 'Manage workers, hiring, onboarding, and HR operations in Workday', + longDescription: + 'Integrate Workday HRIS into your workflow. Create pre-hires, hire employees, manage worker profiles, assign onboarding plans, handle job changes, retrieve compensation data, and process terminations.', + docsLink: 'https://docs.sim.ai/tools/workday', + category: 'tools', + bgColor: '#F5F0EB', + icon: WorkdayIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get Worker', id: 'get_worker' }, + { label: 'List Workers', id: 'list_workers' }, + { label: 'Create Pre-Hire', id: 'create_prehire' }, + { label: 'Hire Employee', id: 'hire_employee' }, + { label: 'Update Worker', id: 'update_worker' }, + { label: 'Assign Onboarding Plan', id: 'assign_onboarding' }, + { label: 'Get Organizations', id: 'get_organizations' }, + { label: 'Change Job', id: 'change_job' }, + { label: 'Get Compensation', id: 'get_compensation' }, + { label: 'Terminate Worker', id: 'terminate_worker' }, + ], + value: () => 'get_worker', + }, + { + id: 'tenantUrl', + title: 'Tenant URL', + type: 'short-input', + placeholder: 'https://wd2-impl-services1.workday.com', + required: true, + description: 'Your Workday instance URL (e.g., https://wd2-impl-services1.workday.com)', + }, + { + id: 'tenant', + title: 'Tenant Name', + type: 'short-input', + placeholder: 'mycompany', + required: true, + description: 'Workday tenant identifier', + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'ISU username', + required: true, + description: 'Integration System User username', + }, + { + id: 'password', + title: 'Password', + type: 'short-input', + placeholder: 'ISU password', + password: true, + required: true, + description: 'Integration System User password', + }, + + // Get Worker + { + id: 'workerId', + title: 'Worker ID', + type: 'short-input', + placeholder: 'e.g., 3aa5550b7fe348b98d7b5741afc65534', + condition: { + field: 'operation', + value: [ + 'get_worker', + 'update_worker', + 'assign_onboarding', + 'change_job', + 'get_compensation', + 'terminate_worker', + ], + }, + required: { + field: 'operation', + value: [ + 'get_worker', + 'update_worker', + 'assign_onboarding', + 'change_job', + 'get_compensation', + 'terminate_worker', + ], + }, + }, + + // List Workers + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: ['list_workers', 'get_organizations'] }, + mode: 'advanced', + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: ['list_workers', 'get_organizations'] }, + mode: 'advanced', + }, + + // Create Pre-Hire + { + id: 'legalName', + title: 'Legal Name', + type: 'short-input', + placeholder: 'e.g., Jane Doe', + condition: { field: 'operation', value: 'create_prehire' }, + required: { field: 'operation', value: 'create_prehire' }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'jane.doe@company.com', + condition: { field: 'operation', value: 'create_prehire' }, + }, + { + id: 'phoneNumber', + title: 'Phone Number', + type: 'short-input', + placeholder: '+1-555-0100', + condition: { field: 'operation', value: 'create_prehire' }, + mode: 'advanced', + }, + { + id: 'address', + title: 'Address', + type: 'short-input', + placeholder: '123 Main St, City, State', + condition: { field: 'operation', value: 'create_prehire' }, + mode: 'advanced', + }, + { + id: 'countryCode', + title: 'Country Code', + type: 'short-input', + placeholder: 'US', + condition: { field: 'operation', value: 'create_prehire' }, + mode: 'advanced', + description: 'ISO 3166-1 Alpha-2 country code (defaults to US)', + }, + + // Hire Employee + { + id: 'preHireId', + title: 'Pre-Hire ID', + type: 'short-input', + placeholder: 'Pre-hire record ID', + condition: { field: 'operation', value: 'hire_employee' }, + required: { field: 'operation', value: 'hire_employee' }, + }, + { + id: 'positionId', + title: 'Position ID', + type: 'short-input', + placeholder: 'Position to assign', + condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + required: { field: 'operation', value: ['hire_employee'] }, + }, + { + id: 'hireDate', + title: 'Hire Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'hire_employee' }, + required: { field: 'operation', value: 'hire_employee' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.', + generationType: 'timestamp', + }, + }, + { + id: 'jobProfileId', + title: 'Job Profile ID', + type: 'short-input', + placeholder: 'Job profile ID', + condition: { field: 'operation', value: 'change_job' }, + mode: 'advanced', + }, + { + id: 'locationId', + title: 'Location ID', + type: 'short-input', + placeholder: 'Work location ID', + condition: { field: 'operation', value: 'change_job' }, + mode: 'advanced', + }, + { + id: 'supervisoryOrgId', + title: 'Supervisory Organization ID', + type: 'short-input', + placeholder: 'Target supervisory organization ID', + condition: { field: 'operation', value: 'change_job' }, + mode: 'advanced', + }, + { + id: 'employeeType', + title: 'Employee Type', + type: 'dropdown', + options: [ + { label: 'Regular', id: 'Regular' }, + { label: 'Temporary', id: 'Temporary' }, + { label: 'Contractor', id: 'Contractor' }, + ], + value: () => 'Regular', + condition: { field: 'operation', value: 'hire_employee' }, + mode: 'advanced', + }, + + // Update Worker + { + id: 'fields', + title: 'Fields (JSON)', + type: 'code', + language: 'json', + placeholder: + '{\n "businessTitle": "Senior Engineer",\n "primaryWorkEmail": "new@company.com"\n}', + condition: { field: 'operation', value: 'update_worker' }, + required: { field: 'operation', value: 'update_worker' }, + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `Generate a Workday worker update payload as JSON. + +### COMMON FIELDS +- businessTitle: Job title string +- primaryWorkEmail: Work email address +- primaryWorkPhone: Work phone number +- managerReference: Manager worker ID + +### RULES +- Output ONLY valid JSON starting with { and ending with } +- Include only fields that need updating + +### EXAMPLE +User: "Update title to Senior Engineer" +Output: {"businessTitle": "Senior Engineer"}`, + generationType: 'json-object', + }, + }, + + // Assign Onboarding + { + id: 'onboardingPlanId', + title: 'Onboarding Plan ID', + type: 'short-input', + placeholder: 'Plan ID to assign', + condition: { field: 'operation', value: 'assign_onboarding' }, + required: { field: 'operation', value: 'assign_onboarding' }, + }, + { + id: 'actionEventId', + title: 'Action Event ID', + type: 'short-input', + placeholder: 'Hiring event ID that enables onboarding', + condition: { field: 'operation', value: 'assign_onboarding' }, + required: { field: 'operation', value: 'assign_onboarding' }, + }, + + // Get Organizations + { + id: 'orgType', + title: 'Organization Type', + type: 'dropdown', + options: [ + { label: 'All Types', id: '' }, + { label: 'Supervisory', id: 'Supervisory' }, + { label: 'Cost Center', id: 'Cost_Center' }, + { label: 'Company', id: 'Company' }, + { label: 'Region', id: 'Region' }, + ], + value: () => '', + condition: { field: 'operation', value: 'get_organizations' }, + }, + + // Change Job + { + id: 'effectiveDate', + title: 'Effective Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'change_job' }, + required: { field: 'operation', value: 'change_job' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.', + generationType: 'timestamp', + }, + }, + { + id: 'reason', + title: 'Reason', + type: 'short-input', + placeholder: 'e.g., Promotion, Transfer', + condition: { field: 'operation', value: ['change_job', 'terminate_worker'] }, + required: { field: 'operation', value: ['change_job', 'terminate_worker'] }, + }, + + // Terminate Worker + { + id: 'terminationDate', + title: 'Termination Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'terminate_worker' }, + required: { field: 'operation', value: 'terminate_worker' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.', + generationType: 'timestamp', + }, + }, + { + id: 'notificationDate', + title: 'Notification Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'terminate_worker' }, + mode: 'advanced', + }, + { + id: 'lastDayOfWork', + title: 'Last Day of Work', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to termination date)', + condition: { field: 'operation', value: 'terminate_worker' }, + mode: 'advanced', + }, + ], + tools: { + access: [ + 'workday_get_worker', + 'workday_list_workers', + 'workday_create_prehire', + 'workday_hire_employee', + 'workday_update_worker', + 'workday_assign_onboarding', + 'workday_get_organizations', + 'workday_change_job', + 'workday_get_compensation', + 'workday_terminate_worker', + ], + config: { + tool: (params) => `workday_${params.operation}`, + params: (params) => { + const { operation, orgType, fields, jobProfileId, locationId, supervisoryOrgId, ...rest } = + params + + if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit) + if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset) + + if (orgType) rest.type = orgType + + if (operation === 'change_job') { + if (rest.positionId) { + rest.newPositionId = rest.positionId + rest.positionId = undefined + } + if (jobProfileId) rest.newJobProfileId = jobProfileId + if (locationId) rest.newLocationId = locationId + if (supervisoryOrgId) rest.newSupervisoryOrgId = supervisoryOrgId + } + + if (fields && operation === 'update_worker') { + try { + const parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields + return { ...rest, fields: parsedFields } + } catch { + throw new Error('Invalid JSON in Fields block') + } + } + + return rest + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Workday operation to perform' }, + tenantUrl: { type: 'string', description: 'Workday instance URL' }, + tenant: { type: 'string', description: 'Workday tenant name' }, + username: { type: 'string', description: 'ISU username' }, + password: { type: 'string', description: 'ISU password' }, + workerId: { type: 'string', description: 'Worker ID' }, + limit: { type: 'number', description: 'Result limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + legalName: { type: 'string', description: 'Legal name for pre-hire' }, + email: { type: 'string', description: 'Email address' }, + phoneNumber: { type: 'string', description: 'Phone number' }, + address: { type: 'string', description: 'Address' }, + countryCode: { type: 'string', description: 'ISO 3166-1 Alpha-2 country code' }, + preHireId: { type: 'string', description: 'Pre-hire record ID' }, + positionId: { type: 'string', description: 'Position ID' }, + hireDate: { type: 'string', description: 'Hire date (YYYY-MM-DD)' }, + jobProfileId: { type: 'string', description: 'Job profile ID' }, + locationId: { type: 'string', description: 'Location ID' }, + supervisoryOrgId: { type: 'string', description: 'Target supervisory organization ID' }, + employeeType: { type: 'string', description: 'Employee type' }, + fields: { type: 'json', description: 'Fields to update' }, + onboardingPlanId: { type: 'string', description: 'Onboarding plan ID' }, + actionEventId: { type: 'string', description: 'Action event ID for onboarding' }, + orgType: { type: 'string', description: 'Organization type filter' }, + effectiveDate: { type: 'string', description: 'Effective date (YYYY-MM-DD)' }, + reason: { type: 'string', description: 'Reason for change or termination' }, + terminationDate: { type: 'string', description: 'Termination date (YYYY-MM-DD)' }, + notificationDate: { type: 'string', description: 'Notification date' }, + lastDayOfWork: { type: 'string', description: 'Last day of work' }, + }, + outputs: { + worker: { type: 'json', description: 'Worker profile data' }, + workers: { type: 'json', description: 'Array of worker profiles' }, + total: { type: 'number', description: 'Total count of results' }, + preHireId: { type: 'string', description: 'Created pre-hire ID' }, + descriptor: { type: 'string', description: 'Display name of pre-hire' }, + workerId: { type: 'string', description: 'Worker ID' }, + employeeId: { type: 'string', description: 'Employee ID' }, + hireDate: { type: 'string', description: 'Hire date' }, + assignmentId: { type: 'string', description: 'Onboarding assignment ID' }, + planId: { type: 'string', description: 'Onboarding plan ID' }, + organizations: { type: 'json', description: 'Array of organizations' }, + eventId: { type: 'string', description: 'Event ID for staffing changes' }, + effectiveDate: { type: 'string', description: 'Effective date of change' }, + compensationPlans: { type: 'json', description: 'Compensation plan details' }, + terminationDate: { type: 'string', description: 'Termination date' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index ee75b893544..9f9f23289ea 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -188,6 +188,7 @@ import { WebhookRequestBlock } from '@/blocks/blocks/webhook_request' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WikipediaBlock } from '@/blocks/blocks/wikipedia' import { WordPressBlock } from '@/blocks/blocks/wordpress' +import { WorkdayBlock } from '@/blocks/blocks/workday' import { WorkflowBlock } from '@/blocks/blocks/workflow' import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input' import { XBlock } from '@/blocks/blocks/x' @@ -408,6 +409,7 @@ export const registry: Record = { whatsapp: WhatsAppBlock, wikipedia: WikipediaBlock, wordpress: WordPressBlock, + workday: WorkdayBlock, workflow: WorkflowBlock, workflow_input: WorkflowInputBlock, x: XBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index a1c6beb37fd..db37ce09e17 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -124,6 +124,34 @@ export function NoteIcon(props: SVGProps) { ) } +export function WorkdayIcon(props: SVGProps) { + const id = useId() + const clipId = `workday_clip_${id}` + return ( + + + + + + + + + + + + ) +} + export function WorkflowIcon(props: SVGProps) { return ( = { wordpress_list_users: wordpressListUsersTool, wordpress_get_user: wordpressGetUserTool, wordpress_search_content: wordpressSearchContentTool, + workday_get_worker: workdayGetWorkerTool, + workday_list_workers: workdayListWorkersTool, + workday_create_prehire: workdayCreatePrehireTool, + workday_hire_employee: workdayHireEmployeeTool, + workday_update_worker: workdayUpdateWorkerTool, + workday_assign_onboarding: workdayAssignOnboardingTool, + workday_get_organizations: workdayGetOrganizationsTool, + workday_change_job: workdayChangeJobTool, + workday_get_compensation: workdayGetCompensationTool, + workday_terminate_worker: workdayTerminateWorkerTool, google_ads_list_customers: googleAdsListCustomersTool, google_ads_search: googleAdsSearchTool, google_ads_list_campaigns: googleAdsListCampaignsTool, diff --git a/apps/sim/tools/workday/assign_onboarding.ts b/apps/sim/tools/workday/assign_onboarding.ts new file mode 100644 index 00000000000..5964a9dd566 --- /dev/null +++ b/apps/sim/tools/workday/assign_onboarding.ts @@ -0,0 +1,93 @@ +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayAssignOnboardingParams, + WorkdayAssignOnboardingResponse, +} from '@/tools/workday/types' + +export const assignOnboardingTool: ToolConfig< + WorkdayAssignOnboardingParams, + WorkdayAssignOnboardingResponse +> = { + id: 'workday_assign_onboarding', + name: 'Assign Workday Onboarding Plan', + description: + 'Create or update an onboarding plan assignment for a worker. Sets up onboarding stages and manages the assignment lifecycle.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + workerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Worker ID to assign the onboarding plan to', + }, + onboardingPlanId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Onboarding plan ID to assign', + }, + actionEventId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Action event ID that enables the onboarding plan (e.g., the hiring event ID)', + }, + }, + + request: { + url: '/api/tools/workday/assign-onboarding', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + assignmentId: { + type: 'string', + description: 'Onboarding plan assignment ID', + }, + workerId: { + type: 'string', + description: 'Worker ID the plan was assigned to', + }, + planId: { + type: 'string', + description: 'Onboarding plan ID that was assigned', + }, + }, +} diff --git a/apps/sim/tools/workday/change_job.ts b/apps/sim/tools/workday/change_job.ts new file mode 100644 index 00000000000..2d986ccd873 --- /dev/null +++ b/apps/sim/tools/workday/change_job.ts @@ -0,0 +1,111 @@ +import type { ToolConfig } from '@/tools/types' +import type { WorkdayChangeJobParams, WorkdayChangeJobResponse } from '@/tools/workday/types' + +export const changeJobTool: ToolConfig = { + id: 'workday_change_job', + name: 'Change Workday Job', + description: + 'Perform a job change for a worker including transfers, promotions, demotions, and lateral moves.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + workerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Worker ID for the job change', + }, + effectiveDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Effective date for the job change in ISO 8601 format (e.g., 2025-06-01)', + }, + newPositionId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New position ID (for transfers)', + }, + newJobProfileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New job profile ID (for role changes)', + }, + newLocationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New work location ID (for relocations)', + }, + newSupervisoryOrgId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Target supervisory organization ID (for org transfers)', + }, + reason: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Reason for the job change (e.g., Promotion, Transfer, Reorganization)', + }, + }, + + request: { + url: '/api/tools/workday/change-job', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + eventId: { + type: 'string', + description: 'Job change event ID', + }, + workerId: { + type: 'string', + description: 'Worker ID the job change was applied to', + }, + effectiveDate: { + type: 'string', + description: 'Effective date of the job change', + }, + }, +} diff --git a/apps/sim/tools/workday/create_prehire.ts b/apps/sim/tools/workday/create_prehire.ts new file mode 100644 index 00000000000..cf787eea437 --- /dev/null +++ b/apps/sim/tools/workday/create_prehire.ts @@ -0,0 +1,101 @@ +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayCreatePrehireParams, + WorkdayCreatePrehireResponse, +} from '@/tools/workday/types' + +export const createPrehireTool: ToolConfig< + WorkdayCreatePrehireParams, + WorkdayCreatePrehireResponse +> = { + id: 'workday_create_prehire', + name: 'Create Workday Pre-Hire', + description: + 'Create a new pre-hire (applicant) record in Workday. This is typically the first step before hiring an employee.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + legalName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full legal name of the pre-hire (e.g., "Jane Doe")', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address of the pre-hire', + }, + phoneNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number of the pre-hire', + }, + address: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Address of the pre-hire', + }, + countryCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 3166-1 Alpha-2 country code (defaults to US)', + }, + }, + + request: { + url: '/api/tools/workday/create-prehire', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + preHireId: { + type: 'string', + description: 'ID of the created pre-hire record', + }, + descriptor: { + type: 'string', + description: 'Display name of the pre-hire', + }, + }, +} diff --git a/apps/sim/tools/workday/get_compensation.ts b/apps/sim/tools/workday/get_compensation.ts new file mode 100644 index 00000000000..9dba3db09a0 --- /dev/null +++ b/apps/sim/tools/workday/get_compensation.ts @@ -0,0 +1,83 @@ +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayGetCompensationParams, + WorkdayGetCompensationResponse, +} from '@/tools/workday/types' + +export const getCompensationTool: ToolConfig< + WorkdayGetCompensationParams, + WorkdayGetCompensationResponse +> = { + id: 'workday_get_compensation', + name: 'Get Workday Compensation', + description: 'Retrieve compensation plan details for a specific worker.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + workerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Worker ID to retrieve compensation data for', + }, + }, + + request: { + url: '/api/tools/workday/get-compensation', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + compensationPlans: { + type: 'array', + description: 'Array of compensation plan details', + items: { + type: 'json', + description: 'Compensation plan with amount, currency, and frequency', + properties: { + id: { type: 'string', description: 'Compensation plan ID' }, + planName: { type: 'string', description: 'Name of the compensation plan' }, + amount: { type: 'number', description: 'Compensation amount' }, + currency: { type: 'string', description: 'Currency code' }, + frequency: { type: 'string', description: 'Pay frequency' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/workday/get_organizations.ts b/apps/sim/tools/workday/get_organizations.ts new file mode 100644 index 00000000000..b2c4bb6acb7 --- /dev/null +++ b/apps/sim/tools/workday/get_organizations.ts @@ -0,0 +1,88 @@ +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayGetOrganizationsParams, + WorkdayGetOrganizationsResponse, +} from '@/tools/workday/types' + +export const getOrganizationsTool: ToolConfig< + WorkdayGetOrganizationsParams, + WorkdayGetOrganizationsResponse +> = { + id: 'workday_get_organizations', + name: 'Get Workday Organizations', + description: 'Retrieve organizations, departments, and cost centers from Workday.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + type: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization type filter (e.g., Supervisory, Cost_Center, Company, Region)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of organizations to return (default: 20)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of records to skip for pagination', + }, + }, + + request: { + url: '/api/tools/workday/get-organizations', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + organizations: { + type: 'array', + description: 'Array of organization records', + }, + total: { + type: 'number', + description: 'Total number of matching organizations', + }, + }, +} diff --git a/apps/sim/tools/workday/get_worker.ts b/apps/sim/tools/workday/get_worker.ts new file mode 100644 index 00000000000..753eff0f9b2 --- /dev/null +++ b/apps/sim/tools/workday/get_worker.ts @@ -0,0 +1,67 @@ +import type { ToolConfig } from '@/tools/types' +import type { WorkdayGetWorkerParams, WorkdayGetWorkerResponse } from '@/tools/workday/types' + +export const getWorkerTool: ToolConfig = { + id: 'workday_get_worker', + name: 'Get Workday Worker', + description: + 'Retrieve a specific worker profile including personal, employment, and organization data.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + workerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Worker ID to retrieve (e.g., 3aa5550b7fe348b98d7b5741afc65534)', + }, + }, + + request: { + url: '/api/tools/workday/get-worker', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + worker: { + type: 'json', + description: 'Worker profile with personal, employment, and organization data', + }, + }, +} diff --git a/apps/sim/tools/workday/hire_employee.ts b/apps/sim/tools/workday/hire_employee.ts new file mode 100644 index 00000000000..f0686ca88d0 --- /dev/null +++ b/apps/sim/tools/workday/hire_employee.ts @@ -0,0 +1,98 @@ +import type { ToolConfig } from '@/tools/types' +import type { WorkdayHireEmployeeParams, WorkdayHireEmployeeResponse } from '@/tools/workday/types' + +export const hireEmployeeTool: ToolConfig = + { + id: 'workday_hire_employee', + name: 'Hire Workday Employee', + description: + 'Hire a pre-hire into an employee position. Converts an applicant into an active employee record with position, start date, and manager assignment.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + preHireId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Pre-hire (applicant) ID to convert into an employee', + }, + positionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Position ID to assign the new hire to', + }, + hireDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Hire date in ISO 8601 format (e.g., 2025-06-01)', + }, + employeeType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Employee type (e.g., Regular, Temporary, Contractor)', + }, + }, + + request: { + url: '/api/tools/workday/hire', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + workerId: { + type: 'string', + description: 'Worker ID of the newly hired employee', + }, + employeeId: { + type: 'string', + description: 'Employee ID assigned to the new hire', + }, + eventId: { + type: 'string', + description: 'Event ID of the hire business process', + }, + hireDate: { + type: 'string', + description: 'Effective hire date', + }, + }, + } diff --git a/apps/sim/tools/workday/index.ts b/apps/sim/tools/workday/index.ts new file mode 100644 index 00000000000..6a93351db52 --- /dev/null +++ b/apps/sim/tools/workday/index.ts @@ -0,0 +1,25 @@ +import { assignOnboardingTool } from '@/tools/workday/assign_onboarding' +import { changeJobTool } from '@/tools/workday/change_job' +import { createPrehireTool } from '@/tools/workday/create_prehire' +import { getCompensationTool } from '@/tools/workday/get_compensation' +import { getOrganizationsTool } from '@/tools/workday/get_organizations' +import { getWorkerTool } from '@/tools/workday/get_worker' +import { hireEmployeeTool } from '@/tools/workday/hire_employee' +import { listWorkersTool } from '@/tools/workday/list_workers' +import { terminateWorkerTool } from '@/tools/workday/terminate_worker' +import { updateWorkerTool } from '@/tools/workday/update_worker' + +export { + assignOnboardingTool as workdayAssignOnboardingTool, + changeJobTool as workdayChangeJobTool, + createPrehireTool as workdayCreatePrehireTool, + getCompensationTool as workdayGetCompensationTool, + getOrganizationsTool as workdayGetOrganizationsTool, + getWorkerTool as workdayGetWorkerTool, + hireEmployeeTool as workdayHireEmployeeTool, + listWorkersTool as workdayListWorkersTool, + terminateWorkerTool as workdayTerminateWorkerTool, + updateWorkerTool as workdayUpdateWorkerTool, +} + +export * from './types' diff --git a/apps/sim/tools/workday/list_workers.ts b/apps/sim/tools/workday/list_workers.ts new file mode 100644 index 00000000000..5009cdff293 --- /dev/null +++ b/apps/sim/tools/workday/list_workers.ts @@ -0,0 +1,76 @@ +import type { ToolConfig } from '@/tools/types' +import type { WorkdayListWorkersParams, WorkdayListWorkersResponse } from '@/tools/workday/types' + +export const listWorkersTool: ToolConfig = { + id: 'workday_list_workers', + name: 'List Workday Workers', + description: 'List or search workers with optional filtering and pagination.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of workers to return (default: 20)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of records to skip for pagination', + }, + }, + + request: { + url: '/api/tools/workday/list-workers', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + workers: { + type: 'array', + description: 'Array of worker profiles', + }, + total: { + type: 'number', + description: 'Total number of matching workers', + }, + }, +} diff --git a/apps/sim/tools/workday/soap.ts b/apps/sim/tools/workday/soap.ts new file mode 100644 index 00000000000..a1f269008f8 --- /dev/null +++ b/apps/sim/tools/workday/soap.ts @@ -0,0 +1,188 @@ +import { createLogger } from '@sim/logger' +import * as soap from 'soap' + +const logger = createLogger('WorkdaySoapClient') + +const WORKDAY_SERVICES = { + staffing: { name: 'Staffing', version: 'v45.1' }, + humanResources: { name: 'Human_Resources', version: 'v45.2' }, + compensation: { name: 'Compensation', version: 'v45.0' }, + recruiting: { name: 'Recruiting', version: 'v45.0' }, +} as const + +export type WorkdayServiceKey = keyof typeof WORKDAY_SERVICES + +export interface WorkdaySoapResult { + Response_Data?: Record + Response_Results?: { + Total_Results?: number + Total_Pages?: number + Page_Results?: number + Page?: number + } + Event_Reference?: WorkdayReference + Employee_Reference?: WorkdayReference + Position_Reference?: WorkdayReference + Applicant_Reference?: WorkdayReference & { attributes?: { Descriptor?: string } } + Onboarding_Plan_Assignment_Reference?: WorkdayReference + Personal_Information_Change_Event_Reference?: WorkdayReference + Exceptions_Response_Data?: unknown +} + +export interface WorkdayReference { + ID?: WorkdayIdEntry[] | WorkdayIdEntry + attributes?: Record +} + +export interface WorkdayIdEntry { + $value?: string + _?: string + attributes?: Record +} + +/** + * Raw SOAP response shape for a single Worker returned by Get_Workers. + * Fields are optional since the Response_Group controls what gets included. + */ +export interface WorkdayWorkerSoap { + Worker_Reference?: WorkdayReference + Worker_Descriptor?: string + Worker_Data?: WorkdayWorkerDataSoap +} + +export interface WorkdayWorkerDataSoap { + Personal_Data?: Record + Employment_Data?: Record + Compensation_Data?: WorkdayCompensationDataSoap + Organization_Data?: Record +} + +export interface WorkdayCompensationDataSoap { + Employee_Base_Pay_Plan_Assignment_Data?: + | WorkdayCompensationPlanSoap + | WorkdayCompensationPlanSoap[] + Employee_Salary_Unit_Plan_Assignment_Data?: + | WorkdayCompensationPlanSoap + | WorkdayCompensationPlanSoap[] + Employee_Bonus_Plan_Assignment_Data?: WorkdayCompensationPlanSoap | WorkdayCompensationPlanSoap[] + Employee_Allowance_Plan_Assignment_Data?: + | WorkdayCompensationPlanSoap + | WorkdayCompensationPlanSoap[] + Employee_Commission_Plan_Assignment_Data?: + | WorkdayCompensationPlanSoap + | WorkdayCompensationPlanSoap[] + Employee_Stock_Plan_Assignment_Data?: WorkdayCompensationPlanSoap | WorkdayCompensationPlanSoap[] + Employee_Period_Salary_Plan_Assignment_Data?: + | WorkdayCompensationPlanSoap + | WorkdayCompensationPlanSoap[] +} + +export interface WorkdayCompensationPlanSoap { + Compensation_Plan_Reference?: WorkdayReference + Amount?: number + Per_Unit_Amount?: number + Individual_Target_Amount?: number + Currency_Reference?: WorkdayReference + Frequency_Reference?: WorkdayReference +} + +/** + * Raw SOAP response shape for a single Organization returned by Get_Organizations. + */ +export interface WorkdayOrganizationSoap { + Organization_Reference?: WorkdayReference + Organization_Descriptor?: string + Organization_Data?: WorkdayOrganizationDataSoap +} + +export interface WorkdayOrganizationDataSoap { + Organization_Type_Reference?: WorkdayReference + Organization_Subtype_Reference?: WorkdayReference + Inactive?: boolean +} + +/** + * Normalizes a SOAP response field that may be a single object, an array, or undefined + * into a consistently typed array. + */ +export function normalizeSoapArray(value: T | T[] | undefined): T[] { + if (!value) return [] + return Array.isArray(value) ? value : [value] +} + +type SoapOperationFn = ( + args: Record +) => Promise<[WorkdaySoapResult, string, Record, string]> + +export interface WorkdayClient extends soap.Client { + Get_WorkersAsync: SoapOperationFn + Get_OrganizationsAsync: SoapOperationFn + Put_ApplicantAsync: SoapOperationFn + Hire_EmployeeAsync: SoapOperationFn + Change_JobAsync: SoapOperationFn + Terminate_EmployeeAsync: SoapOperationFn + Change_Personal_InformationAsync: SoapOperationFn + Put_Onboarding_Plan_AssignmentAsync: SoapOperationFn +} + +/** + * Builds the WSDL URL for a Workday SOAP service. + * Pattern: {tenantUrl}/ccx/service/{tenant}/{serviceName}/{version}?wsdl + */ +export function buildWsdlUrl( + tenantUrl: string, + tenant: string, + service: WorkdayServiceKey +): string { + const svc = WORKDAY_SERVICES[service] + const baseUrl = tenantUrl.replace(/\/$/, '') + return `${baseUrl}/ccx/service/${tenant}/${svc.name}/${svc.version}?wsdl` +} + +/** + * Creates a typed SOAP client for a Workday service. + * Uses the `soap` npm package to parse the WSDL and auto-marshall JSON to XML. + */ +export async function createWorkdaySoapClient( + tenantUrl: string, + tenant: string, + service: WorkdayServiceKey, + username: string, + password: string +): Promise { + const wsdlUrl = buildWsdlUrl(tenantUrl, tenant, service) + logger.info('Creating Workday SOAP client', { service, wsdlUrl }) + + const client = await soap.createClientAsync(wsdlUrl) + client.setSecurity(new soap.BasicAuthSecurity(username, password)) + return client as WorkdayClient +} + +/** + * Builds a Workday object reference in the format the SOAP API expects. + * Generates: { ID: { attributes: { type: idType }, $value: idValue } } + */ +export function wdRef(idType: string, idValue: string): { ID: WorkdayIdEntry } { + return { + ID: { + attributes: { 'wd:type': idType }, + $value: idValue, + }, + } +} + +/** + * Extracts a reference ID from a SOAP response object. + * Handles the nested ID structure that Workday returns. + */ +export function extractRefId(ref: WorkdayReference | undefined): string | null { + if (!ref) return null + const id = ref.ID + if (Array.isArray(id)) { + return id[0]?.$value ?? id[0]?._ ?? null + } + if (id && typeof id === 'object') { + return id.$value ?? id._ ?? null + } + return null +} diff --git a/apps/sim/tools/workday/terminate_worker.ts b/apps/sim/tools/workday/terminate_worker.ts new file mode 100644 index 00000000000..af2a8094bb4 --- /dev/null +++ b/apps/sim/tools/workday/terminate_worker.ts @@ -0,0 +1,105 @@ +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayTerminateWorkerParams, + WorkdayTerminateWorkerResponse, +} from '@/tools/workday/types' + +export const terminateWorkerTool: ToolConfig< + WorkdayTerminateWorkerParams, + WorkdayTerminateWorkerResponse +> = { + id: 'workday_terminate_worker', + name: 'Terminate Workday Worker', + description: + 'Initiate a worker termination in Workday. Triggers the Terminate Employee business process.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + workerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Worker ID to terminate', + }, + terminationDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Termination date in ISO 8601 format (e.g., 2025-06-01)', + }, + reason: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Termination reason (e.g., Resignation, End_of_Contract, Retirement)', + }, + notificationDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Date the termination was communicated in ISO 8601 format', + }, + lastDayOfWork: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last day of work in ISO 8601 format (defaults to termination date)', + }, + }, + + request: { + url: '/api/tools/workday/terminate', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + eventId: { + type: 'string', + description: 'Termination event ID', + }, + workerId: { + type: 'string', + description: 'Worker ID that was terminated', + }, + terminationDate: { + type: 'string', + description: 'Effective termination date', + }, + }, +} diff --git a/apps/sim/tools/workday/types.ts b/apps/sim/tools/workday/types.ts new file mode 100644 index 00000000000..bc77a33a0ee --- /dev/null +++ b/apps/sim/tools/workday/types.ts @@ -0,0 +1,183 @@ +import type { ToolResponse } from '@/tools/types' + +export interface WorkdayBaseParams { + tenantUrl: string + tenant: string + username: string + password: string +} + +export interface WorkdayWorker { + id: string + descriptor: string + primaryWorkEmail?: string + primaryWorkPhone?: string + businessTitle?: string + supervisoryOrganization?: string + hireDate?: string + workerType?: string + isActive?: boolean + [key: string]: unknown +} + +export interface WorkdayOrganization { + id: string + descriptor: string + type?: string + subtype?: string + isActive?: boolean + [key: string]: unknown +} + +/** Get Worker */ +export interface WorkdayGetWorkerParams extends WorkdayBaseParams { + workerId: string +} + +export interface WorkdayGetWorkerResponse extends ToolResponse { + output: { + worker: WorkdayWorker + } +} + +/** List Workers */ +export interface WorkdayListWorkersParams extends WorkdayBaseParams { + limit?: number + offset?: number +} + +export interface WorkdayListWorkersResponse extends ToolResponse { + output: { + workers: WorkdayWorker[] + total: number + } +} + +/** Create Pre-Hire */ +export interface WorkdayCreatePrehireParams extends WorkdayBaseParams { + legalName: string + email?: string + phoneNumber?: string + address?: string + countryCode?: string +} + +export interface WorkdayCreatePrehireResponse extends ToolResponse { + output: { + preHireId: string + descriptor: string + } +} + +/** Hire Employee */ +export interface WorkdayHireEmployeeParams extends WorkdayBaseParams { + preHireId: string + positionId: string + hireDate: string + employeeType?: string +} + +export interface WorkdayHireEmployeeResponse extends ToolResponse { + output: { + workerId: string + employeeId: string + eventId: string + hireDate: string + } +} + +/** Update Worker */ +export interface WorkdayUpdateWorkerParams extends WorkdayBaseParams { + workerId: string + fields: Record +} + +export interface WorkdayUpdateWorkerResponse extends ToolResponse { + output: { + eventId: string + workerId: string + } +} + +/** Assign Onboarding Plan */ +export interface WorkdayAssignOnboardingParams extends WorkdayBaseParams { + workerId: string + onboardingPlanId: string + actionEventId: string +} + +export interface WorkdayAssignOnboardingResponse extends ToolResponse { + output: { + assignmentId: string + workerId: string + planId: string + } +} + +/** Get Organizations */ +export interface WorkdayGetOrganizationsParams extends WorkdayBaseParams { + type?: string + limit?: number + offset?: number +} + +export interface WorkdayGetOrganizationsResponse extends ToolResponse { + output: { + organizations: WorkdayOrganization[] + total: number + } +} + +/** Change Job */ +export interface WorkdayChangeJobParams extends WorkdayBaseParams { + workerId: string + effectiveDate: string + newPositionId?: string + newJobProfileId?: string + newLocationId?: string + newSupervisoryOrgId?: string + reason: string +} + +export interface WorkdayChangeJobResponse extends ToolResponse { + output: { + eventId: string + workerId: string + effectiveDate: string + } +} + +/** Get Compensation */ +export interface WorkdayGetCompensationParams extends WorkdayBaseParams { + workerId: string +} + +export interface WorkdayGetCompensationResponse extends ToolResponse { + output: { + compensationPlans: Array<{ + id: string + planName: string + amount: number + currency: string + frequency: string + [key: string]: unknown + }> + } +} + +/** Terminate Worker */ +export interface WorkdayTerminateWorkerParams extends WorkdayBaseParams { + workerId: string + terminationDate: string + reason: string + notificationDate?: string + lastDayOfWork?: string +} + +export interface WorkdayTerminateWorkerResponse extends ToolResponse { + output: { + eventId: string + workerId: string + terminationDate: string + } +} diff --git a/apps/sim/tools/workday/update_worker.ts b/apps/sim/tools/workday/update_worker.ts new file mode 100644 index 00000000000..0c14427b714 --- /dev/null +++ b/apps/sim/tools/workday/update_worker.ts @@ -0,0 +1,78 @@ +import type { ToolConfig } from '@/tools/types' +import type { WorkdayUpdateWorkerParams, WorkdayUpdateWorkerResponse } from '@/tools/workday/types' + +export const updateWorkerTool: ToolConfig = + { + id: 'workday_update_worker', + name: 'Update Workday Worker', + description: 'Update fields on an existing worker record in Workday.', + version: '1.0.0', + + params: { + tenantUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)', + }, + tenant: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Workday tenant name', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', + }, + workerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Worker ID to update', + }, + fields: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Fields to update as JSON (e.g., {"businessTitle": "Senior Engineer", "primaryWorkEmail": "new@company.com"})', + }, + }, + + request: { + url: '/api/tools/workday/update-worker', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => params, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') + } + return data + }, + + outputs: { + eventId: { + type: 'string', + description: 'Event ID of the change personal information business process', + }, + workerId: { + type: 'string', + description: 'Worker ID that was updated', + }, + }, + } diff --git a/bun.lock b/bun.lock index 61df0c93763..9e3efc4d447 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -185,6 +184,7 @@ "resend": "^4.1.2", "rss-parser": "3.13.0", "sharp": "0.34.3", + "soap": "1.8.0", "socket.io": "^4.8.1", "socket.io-client": "4.8.1", "ssh2": "^1.17.0", @@ -952,6 +952,8 @@ "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + "@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="], "@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="], @@ -1690,6 +1692,8 @@ "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], @@ -1712,7 +1716,9 @@ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + + "axios-ntlm": ["axios-ntlm@1.4.6", "", { "dependencies": { "axios": "^1.12.2", "des.js": "^1.1.0", "dev-null": "^0.1.1", "js-md4": "^0.3.2" } }, "sha512-4nR5cbVEBfPMTFkd77FEDpDuaR205JKibmrkaQyNwGcCx0szWNpRZaL0jZyMx4+mVY2PXHjRHuJafv9Oipl0Kg=="], "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], @@ -2020,6 +2026,8 @@ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], @@ -2028,10 +2036,14 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "dev-null": ["dev-null@0.1.1", "", {}, "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "devtools-protocol": ["devtools-protocol@0.0.1464554", "", {}, "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], @@ -2252,6 +2264,8 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], @@ -2500,6 +2514,8 @@ "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], @@ -2786,6 +2802,8 @@ "minimal-polyfills": ["minimal-polyfills@2.2.3", "", {}, "sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + "minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -3242,7 +3260,7 @@ "satori": ["satori@0.12.2", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.16", "css-to-react-native": "^3.0.0", "emoji-regex": "^10.2.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-wasm-web": "^0.3.3" } }, "sha512-3C/laIeE6UUe9A+iQ0A48ywPVCCMKCNSTU5Os101Vhgsjd3AAxGNjyq0uAA8kulMPK5n0csn8JlxPN9riXEjLA=="], - "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], @@ -3318,6 +3336,8 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "soap": ["soap@1.8.0", "", { "dependencies": { "axios": "^1.13.6", "axios-ntlm": "^1.4.6", "debug": "^4.4.3", "follow-redirects": "^1.15.11", "formidable": "^3.5.4", "sax": "^1.5.0", "whatwg-mimetype": "4.0.0", "xml-crypto": "^6.1.2" } }, "sha512-WRIzZm4M13a9j1t8yMdZZtbbkxNatXAhvtO8UXc/LvdfZ/Op1MqZS6qsAbILLsLTk3oLM/PRw0XOG0U53dAZzg=="], + "socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], "socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="], @@ -3914,6 +3934,8 @@ "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], + "@paralleldrive/cuid2/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -4326,6 +4348,8 @@ "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "twilio/axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "twilio/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "twilio/xmlbuilder": ["xmlbuilder@13.0.2", "", {}, "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="], @@ -4344,6 +4368,10 @@ "xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="], + "xml-js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + + "xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],