feat(workday): block + tools (#3663)

* checkpoint workday block

* add icon svg

* fix workday to use soap api

* fix SOAP API

* address comments

* fix

* more type fixes

* address more comments

* fix files

* fix file editor useEffect

* fix build issue

* fix typing

* fix test
This commit is contained in:
Vikhyath Mondreti
2026-03-18 22:26:10 -07:00
committed by GitHub
parent 12908c14be
commit bc111a6d5c
35 changed files with 2762 additions and 110 deletions

View File

@@ -102,7 +102,7 @@ async function handleLocalFile(filename: string, userId: string): Promise<NextRe
throw new FileNotFoundError(`File not found: ${filename}`)
}
const filePath = findLocalFile(filename)
const filePath = await findLocalFile(filename)
if (!filePath) {
throw new FileNotFoundError(`File not found: ${filename}`)
@@ -228,7 +228,7 @@ async function handleCloudProxyPublic(
async function handleLocalFilePublic(filename: string): Promise<NextResponse> {
try {
const filePath = findLocalFile(filename)
const filePath = await findLocalFile(filename)
if (!filePath) {
throw new FileNotFoundError(`File not found: ${filename}`)

View File

@@ -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'

View File

@@ -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',
]
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',
]
legitimateInputs.forEach((input) => {
// Should not throw security errors for legitimate filenames
expect(() => findLocalFile(input)).not.toThrow()
})
})
for (const input of legitimateInputs) {
await expect(findLocalFile(input)).resolves.toBeDefined()
}
}
)
})
})

View File

@@ -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<string | null> {
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 (
!resolvedPath.startsWith(UPLOAD_DIR_SERVER + path.sep) ||
resolvedPath === UPLOAD_DIR_SERVER
) {
return null
}
if (!isWithinAllowedDir) {
continue
}
if (existsSync(resolvedPath)) {
return resolvedPath
}
if (existsSync(resolvedPath)) {
return resolvedPath
}
return null

View File

@@ -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 }
)
}
}

View File

@@ -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<string, unknown> = {
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 }
)
}
}

View File

@@ -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<string, unknown> = {}
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 }
)
}
}

View File

@@ -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<typeof mapPlan>[] = []
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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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) {

View File

@@ -151,6 +151,8 @@ export function Files() {
}
const justCreatedFileIdRef = useRef<string | null>(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

View File

@@ -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' },
},
}

View File

@@ -189,6 +189,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'
@@ -410,6 +411,7 @@ export const registry: Record<string, BlockConfig> = {
whatsapp: WhatsAppBlock,
wikipedia: WikipediaBlock,
wordpress: WordPressBlock,
workday: WorkdayBlock,
workflow: WorkflowBlock,
workflow_input: WorkflowInputBlock,
x: XBlock,

View File

@@ -124,6 +124,34 @@ export function NoteIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function WorkdayIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const clipId = `workday_clip_${id}`
return (
<svg {...props} viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'>
<g clipPath={`url(#${clipId})`} transform='matrix(0.53333333,0,0,0.53333333,-124.63685,-16)'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='m 251.21,88.7755 h 8.224 c 1.166,0 2.178,0.7836 2.444,1.8924 l 11.057,44.6751 c 0.152,0.002 12.182,-44.6393 12.182,-44.6393 0.306,-1.1361 1.36,-1.9282 2.566,-1.9282 h 12.74 c 1.144,0 2.144,0.7515 2.435,1.8296 l 12.118,44.9289 c 0.448,-0.282 11.147,-44.8661 11.147,-44.8661 0.267,-1.1088 1.279,-1.8924 2.444,-1.8924 h 8.219 c 1.649,0 2.854,1.5192 2.437,3.0742 l -15.08,56.3173 c -0.286,1.072 -1.272,1.823 -2.406,1.833 l -12.438,-0.019 c -1.142,-0.002 -2.137,-0.744 -2.429,-1.819 -2.126,-7.805 -12.605,-47.277 -12.605,-47.277 0,0 -11.008,39.471 -13.133,47.277 -0.293,1.075 -1.288,1.817 -2.429,1.819 L 266.264,150 c -1.133,-0.01 -2.119,-0.761 -2.406,-1.833 L 248.777,91.8438 c -0.416,-1.5524 0.786,-3.0683 2.433,-3.0683 z'
fill='#005cb9'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='m 333.324,72.2449 c 0.531,0 1.071,-0.0723 1.608,-0.2234 3.18,-0.8968 5.039,-4.2303 4.153,-7.446 -0.129,-0.4673 -0.265,-0.9327 -0.408,-1.3936 C 332.529,43.3349 314.569,30 293.987,30 c -20.557,0 -38.51,13.3133 -44.673,33.1281 -0.136,0.4355 -0.267,0.8782 -0.391,1.3232 -0.902,3.2119 0.943,6.5541 4.12,7.4645 3.173,0.9112 6.48,-0.9547 7.381,-4.1666 0.094,-0.3322 0.19,-0.6616 0.292,-0.9892 4.591,-14.7582 17.961,-24.6707 33.271,-24.6707 15.329,0 28.704,9.9284 33.281,24.7063 0.105,0.3397 0.206,0.682 0.301,1.0263 0.737,2.6726 3.139,4.423 5.755,4.423 z'
fill='#f38b00'
/>
</g>
<defs>
<clipPath id={clipId}>
<path d='M 354,30 H 234 v 120 h 120 z' fill='#ffffff' />
</clipPath>
</defs>
</svg>
)
}
export function WorkflowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -159,6 +159,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",

View File

@@ -2280,6 +2280,18 @@ import {
wordpressUpdatePostTool,
wordpressUploadMediaTool,
} from '@/tools/wordpress'
import {
workdayAssignOnboardingTool,
workdayChangeJobTool,
workdayCreatePrehireTool,
workdayGetCompensationTool,
workdayGetOrganizationsTool,
workdayGetWorkerTool,
workdayHireEmployeeTool,
workdayListWorkersTool,
workdayTerminateWorkerTool,
workdayUpdateWorkerTool,
} from '@/tools/workday'
import { workflowExecutorTool } from '@/tools/workflow'
import {
xCreateBookmarkTool,
@@ -4120,6 +4132,16 @@ export const tools: Record<string, ToolConfig> = {
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,

View File

@@ -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',
},
},
}

View File

@@ -0,0 +1,111 @@
import type { ToolConfig } from '@/tools/types'
import type { WorkdayChangeJobParams, WorkdayChangeJobResponse } from '@/tools/workday/types'
export const changeJobTool: ToolConfig<WorkdayChangeJobParams, WorkdayChangeJobResponse> = {
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',
},
},
}

View File

@@ -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',
},
},
}

View File

@@ -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' },
},
},
},
},
}

View File

@@ -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',
},
},
}

View File

@@ -0,0 +1,67 @@
import type { ToolConfig } from '@/tools/types'
import type { WorkdayGetWorkerParams, WorkdayGetWorkerResponse } from '@/tools/workday/types'
export const getWorkerTool: ToolConfig<WorkdayGetWorkerParams, WorkdayGetWorkerResponse> = {
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',
},
},
}

View File

@@ -0,0 +1,98 @@
import type { ToolConfig } from '@/tools/types'
import type { WorkdayHireEmployeeParams, WorkdayHireEmployeeResponse } from '@/tools/workday/types'
export const hireEmployeeTool: ToolConfig<WorkdayHireEmployeeParams, WorkdayHireEmployeeResponse> =
{
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',
},
},
}

View File

@@ -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'

View File

@@ -0,0 +1,76 @@
import type { ToolConfig } from '@/tools/types'
import type { WorkdayListWorkersParams, WorkdayListWorkersResponse } from '@/tools/workday/types'
export const listWorkersTool: ToolConfig<WorkdayListWorkersParams, WorkdayListWorkersResponse> = {
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',
},
},
}

View File

@@ -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<string, unknown>
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<string, string>
}
export interface WorkdayIdEntry {
$value?: string
_?: string
attributes?: Record<string, string>
}
/**
* 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<string, unknown>
Employment_Data?: Record<string, unknown>
Compensation_Data?: WorkdayCompensationDataSoap
Organization_Data?: Record<string, unknown>
}
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<T>(value: T | T[] | undefined): T[] {
if (!value) return []
return Array.isArray(value) ? value : [value]
}
type SoapOperationFn = (
args: Record<string, unknown>
) => Promise<[WorkdaySoapResult, string, Record<string, unknown>, 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<WorkdayClient> {
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
}

View File

@@ -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',
},
},
}

View File

@@ -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<string, unknown>
}
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
}
}

View File

@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { WorkdayUpdateWorkerParams, WorkdayUpdateWorkerResponse } from '@/tools/workday/types'
export const updateWorkerTool: ToolConfig<WorkdayUpdateWorkerParams, WorkdayUpdateWorkerResponse> =
{
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',
},
},
}

View File

@@ -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=="],