Skip to content

[Alpha] SyncFactors - SF to AD Sync

SyncFactors SyncFactors

Local-first synchronization from SuccessFactors HR data to Microsoft Active Directory.
ASP.NET Core operator portal, background worker execution, SQLite runtime state, and review-first directory sync workflows.

Current StateLocal DevelopmentConfig ModelArchitectureEmpJob Mapping

Tests Security Scans CodeQL Quality Gate Status License Last Commit Top Language

SyncFactors is a local-first .NET tool for syncing SuccessFactors HR data into Microsoft Active Directory.

Warning

[Alpha] This software is in active development, has a high risk of failure, and is not ready for production use. Expect breaking changes, incomplete workflows, missing features, and operational defects. Validate everything in a non-production environment first.

Current State

  • The active implementation is a local-first .NET 10 solution built around an ASP.NET Core operator portal/API, a background worker, SQLite-backed runtime state, and a local mock SuccessFactors service.
  • The repository already contains operator-facing dashboard, sync, exception, Worker 360, lookup, user access, deletion queue, and configuration pages.
  • Runtime workflows include run queueing, cancellation, recurring full-sync scheduling, dependency health probes, saved worker preview/apply, graveyard deletion handling, lifecycle simulation, and production dry-run enforcement.
  • Authentication supports local break-glass, OIDC-only, and hybrid OIDC plus break-glass modes.
  • Production readiness is not implied by the current feature set, repository layout, sample config, or helper scripts.

Current dashboard snapshot:

SyncFactors operator dashboard SyncFactors operator dashboard

Goals

  • Keep the product local-first and operator-friendly.
  • Preserve dry-run, review, approvals, rollback, and auditability as first-class capabilities.
  • Replace shell-driven orchestration with typed domain services and explicit job/state models where practical.
  • Maintain a path from a single-tenant Windows admin tool to a future hosted control plane if that becomes necessary.

Current Stack

  • Runtime: .NET 10
  • Operator UI: ASP.NET Core Razor Pages plus a bundled browser layer for realtime updates and analytics
  • Frontend tooling: npm plus esbuild
  • Frontend runtime packages: SignalR, Motion, and ECharts
  • Background execution: .NET hosted worker service
  • Runtime state: SQLite
  • Directory integration: .NET plus PowerShell seams where needed
  • Tests: xUnit across API, automation, domain, infrastructure, and mock SuccessFactors projects, plus Vitest coverage for the browser bundle

Solution Shape

  • src/SyncFactors.Api: local operator UI plus authenticated JSON endpoints for status, dashboard, health, runs, schedule management, and a realtime SignalR dashboard hub
  • src/SyncFactors.Worker: background host that claims queued runs, executes sync work, records heartbeats, and processes recurring schedules
  • src/SyncFactors.MockSuccessFactors: local SuccessFactors-like API plus fixture generation tooling for development and testing
  • src/SyncFactors.Automation: scenario runner and local automation-user bootstrap CLI used by E2E and readiness scripts
  • src/SyncFactors.Domain: run orchestration, preview/apply behavior, lifecycle rules, scheduling, and sync coordination
  • src/SyncFactors.Infrastructure: SQLite persistence, Active Directory access, SuccessFactors client logic, authentication and local user storage, filesystem helpers, and config loading
  • src/SyncFactors.Contracts: shared runtime DTOs and status models
  • tests/*: unit and integration test projects aligned to the runtime components above
  • config/*: tracked sample config, mock fixture data, and scaffold data
  • docs/architecture.md: current implementation architecture and system boundaries
  • docs/empjob-ad-mapping.md: current field mapping notes for the EmpJob flow

What Works Today

  • Operator dashboard with current runtime status, recent runs, active run summary, dependency health, and realtime updates over SignalR
  • Live dashboard visuals including run mix and bucket composition charts, a sticky live status rail, and a run timeline card
  • Ad hoc run queueing for dry-run and live syncs, plus cancellation for queued or active runs
  • Recurring full-sync schedule configuration backed by SQLite
  • Run history and run detail pages
  • Worker preview flow that stages one worker, persists the preview, and supports explicit apply from the saved fingerprint
  • Worker 360 view with source summary, directory match, provisioning decision tree, preview history, previous-preview comparison, apply readiness, attribute diff, source confidence, run history, and preview entries
  • Exceptions queue for failed runs, manual review items, conflicts, and guardrail failures
  • Admin deletion queue for graveyard users with pending and on-hold states
  • Admin configuration snapshot for deployment, auth, SuccessFactors, Active Directory, operations, safety, alerts, and mappings
  • Authentication modes for local break-glass, OIDC-only, or hybrid SSO plus break-glass access, with local user management when break-glass is enabled
  • Deployment-level dry-run-only mode that blocks live writes in the API and worker, queues scheduled runs as dry runs, removes live-write controls, and displays a persistent UI banner
  • Mock SuccessFactors API for local development, fixture playback, and synthetic worker population
  • Lifecycle simulator for deterministic employee-state progression without a live SuccessFactors endpoint
  • Development-only delete-all testing reset flow from the Sync page that clears configured AD test OUs
  • Active Directory health checks that validate lookup behavior across configured search bases instead of only doing a bind/base-object probe
  • Due graveyard retention report processing from the worker when alerts are configured

Caution

The delete-all/testing reset flow is destructive. It exists for controlled development testing, requires administrative access, and should be treated as dangerous even in non-production environments.

Status

The solution builds from the repository root against the locally installed .NET 10 SDK.

The repository is still alpha. The implementation is concrete enough to document current operator flows, but APIs, config shape, storage details, and operational behavior may still change materially.

Local Development

Primary commands from the repository root:

dotnet build ./SyncFactors.Next.sln
dotnet test ./SyncFactors.Next.sln

Run the local validation entrypoint:

pwsh ./scripts/Validate-SyncFactors.ps1

That command builds the solution, runs the solution test suite, and then runs the lifecycle simulation master suite as an explicit final gate.

When changing the operator browser bundle, also run the frontend checks from src/SyncFactors.Api:

cd ./src/SyncFactors.Api
npm ci --ignore-scripts
npm run test:ui
npm run build:ui

Windows Release Bundle

The release workflow publishes a self-contained Windows x64 deployment zip to GitHub Releases. The asset is named like syncfactors-<version>-win-x64.zip and includes the API, worker, sample config, docs, and scripts.

After extracting the zip on a Windows host, start the applications from the bundle root:

.\app\api\SyncFactors.Api.exe
.\app\worker\SyncFactors.Worker.exe

The bundle is self-contained, so the target Windows host does not need a separate .NET runtime install.

To install the API and worker as Windows Services, run an elevated PowerShell session from the extracted bundle root:

pwsh .\scripts\Install-SyncFactorsWindowsServices.ps1 `
  -RunProfile real `
  -ApiUrls https://127.0.0.1:5087

Start-Service SyncFactors.Api
Start-Service SyncFactors.Worker

The installer creates SyncFactors.Api and SyncFactors.Worker, registers matching Windows Event Log sources under the Application log, configures restart-on-failure recovery, and writes service environment values for the selected profile, config paths, SQLite path, security audit log path, SQLite encryption password, and local file logging. It also creates local config files from the bundled samples when they are missing. SQLite encryption is enabled by default for Windows Services; pass -SqlitePassword to provide a managed SQLCipher key, or let the installer generate one and store it in the service environment. To replace existing service definitions, rerun with -Force; the installer reuses the existing service password when present.

Uninstall the services from an elevated session:

pwsh .\scripts\Uninstall-SyncFactorsWindowsServices.ps1

Use Event Viewer > Windows Logs > Application with sources SyncFactors.Api and SyncFactors.Worker for service startup, shutdown, warning, and error events. Local rolling logs are also enabled by default under state\logs inside the bundle root.

Repeatable Windows Patch Deployment

For QA and production patching, bootstrap the server once, then deploy new release zips with scripts/Deploy-SyncFactorsWindowsPatch.ps1. The patch deploy keeps the durable server-owned paths intact:

  • C:\SyncFactors\config\local.*
  • C:\SyncFactors\state\runtime\syncfactors.db
  • C:\SyncFactors\state\logs
  • Windows Credential Manager secrets
  • the installed service account and HTTPS certificate

Fresh server setup uses four durable layers:

  1. Extract the release bundle to C:\SyncFactors.
  2. Prepare the Windows service account, directory ACLs, PowerShell 7, and optional firewall rule.
  3. Configure the HTTPS certificate and the app secrets under the Windows identity that runs the services.
  4. Install SyncFactors.Api and SyncFactors.Worker.

Windows Credential Manager is still the preferred place for SuccessFactors, AD, OIDC, and break-glass secrets for a service deployment, but it is per Windows identity. Values saved while logged on as a deploy/admin account are not visible to SyncFactors.Api or SyncFactors.Worker when those services run as sfsvc or a domain runtime account. The service resolver checks process environment variables first, then Windows Credential Manager target names like SyncFactors/SF_AD_SYNC_SF_USERNAME in the service account profile. The service installer writes operational settings such as config paths, log paths, TLS thumbprint, SQLite path, and SQLite encryption password into the service registry environment; app credentials should stay in Windows Credential Manager unless there is an operational reason to place them directly in the service environment.

First-time server setup still uses the prerequisite and service installers:

Set-ExecutionPolicy -Scope Process Bypass
$password = Read-Host 'SyncFactors service password' -AsSecureString

.\scripts\Install-SyncFactorsWindowsPrerequisites.ps1 `
  -InstallRoot C:\SyncFactors `
  -ServiceAccount sfsvc `
  -ServiceAccountPassword $password `
  -CreateLocalServiceAccount `
  -InstallPowerShell `
  -ConfigureFirewall `
  -ApiPort 5087

$credential = [pscredential]::new("$env:COMPUTERNAME\sfsvc", $password)

For a CA-issued or enterprise PFX certificate, import it into LocalMachine\My and grant the runtime account read access to the private key:

$pfxPassword = Read-Host 'SyncFactors HTTPS PFX password' -AsSecureString
$pfxPasswordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($pfxPassword)
try {
  $pfxPasswordPlain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pfxPasswordPointer)

  .\scripts\Install-SyncFactorsHttpsCertificateFromPfx.ps1 `
    -PfxPath C:\SyncFactors\certs\syncfactors-api.pfx `
    -PfxPassword $pfxPasswordPlain `
    -StoreLocation LocalMachine `
    -ServiceAccount "$env:COMPUTERNAME\sfsvc"
}
finally {
  [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pfxPasswordPointer)
  Remove-Variable pfxPasswordPlain -ErrorAction SilentlyContinue
}

$tlsThumbprint = (Get-PfxCertificate C:\SyncFactors\certs\syncfactors-api.pfx).Thumbprint

If the certificate is already in LocalMachine\My, set $tlsThumbprint to its thumbprint and make sure the service account can read the private key.

Store real profile secrets under the runtime service identity. This opens a PowerShell prompt as sfsvc, loads that account profile, and writes Credential Manager targets such as SyncFactors/SF_AD_SYNC_SF_USERNAME:

Start-Process pwsh `
  -Credential $credential `
  -LoadUserProfile `
  -ArgumentList @(
    '-NoProfile',
    '-ExecutionPolicy', 'Bypass',
    '-NoExit',
    '-Command',
    'Set-Location C:\SyncFactors; .\scripts\Set-SyncFactorsWindowsCredential.ps1 -VariableName SF_AD_SYNC_AD_SERVER; .\scripts\Set-SyncFactorsWindowsCredential.ps1 -VariableName SF_AD_SYNC_SF_USERNAME; .\scripts\Set-SyncFactorsWindowsCredential.ps1 -VariableName SF_AD_SYNC_SF_PASSWORD'
  )

Then install the services from the elevated deploy/admin session:

.\scripts\Install-SyncFactorsWindowsServices.ps1 `
  -BundleRoot C:\SyncFactors `
  -RunProfile real `
  -ApiUrls 'https://0.0.0.0:5087' `
  -TlsCertificateThumbprint $tlsThumbprint `
  -WindowsCredentialPrefix SyncFactors `
  -Credential $credential `
  -Force

After the services exist, deploy each incremental QA or production bundle from an elevated PowerShell session:

.\scripts\Deploy-SyncFactorsWindowsPatch.ps1 `
  -BundleZip C:\SyncFactors\_staging\syncfactors-<version>-win-x64.zip `
  -InstallRoot C:\SyncFactors `
  -HealthUrl https://localhost:5087/Login

The patch script expands the bundle to _staging, backs up deployable files to _backups\<timestamp>, stops SyncFactors.Api and SyncFactors.Worker, replaces app, scripts, docs, and sample config files, restarts services, and health-checks the API. It does not overwrite config\local.* or state. If startup or the health check fails, it restores the backed-up deployable files and restarts the previous version unless -NoRollbackOnFailure is set.

Use -InstallOrUpdateServices -Credential $credential only when the patch must create or replace the Windows service definitions. Existing service credentials cannot be recovered from Windows, so that mode requires the credential again.

Azure DevOps Windows Deployment

azure-pipelines.deploy.yml builds, tests, packages, and deploys the self-contained Windows bundle to one or more Windows servers over WinRM. The deploy stage is skipped until deployTargetMachines is set in the Azure DevOps pipeline variables.

Configure these Azure DevOps variables before enabling deployment:

Variable Purpose
deployTargetMachines Comma-separated Windows server DNS names or IP addresses reachable over WinRM.
deployAdminUserName / deployAdminPassword Local or domain administrator credential used by Azure DevOps for copy/install tasks. Mark the password secret.
installRoot Target install directory. Defaults to C:\SyncFactors.
remoteStagingPath Temporary bundle copy path. Defaults to C:\SyncFactors\_staging.
runProfile Service profile passed to the installer. Defaults to real.
apiUrls Kestrel bind URL for the API Windows Service. Defaults to https://127.0.0.1:5087.
tlsCertificateThumbprint Optional LocalMachine\My certificate thumbprint for the API HTTPS binding. If omitted and no PFX path/password is supplied, the installer tries to use a usable machine certificate matching apiUrls, SYNCFACTORS_API_PUBLIC_HOST, or the machine name.
configureFirewall Set to true to create or update an inbound Domain/Private firewall rule for apiPort.
createLocalServiceAccount Set to true to create serviceUserName as a local Windows runtime account before installing services. Leave false for a pre-created domain account.
windowsCredentialPrefix Windows Credential Manager target prefix used by the services. Defaults to SyncFactors.
serviceUserName / serviceUserPassword Runtime Windows service credential for SyncFactors.Api and SyncFactors.Worker. Mark the password secret.
sqlitePassword Optional override for the runtime SQLCipher password. If omitted, the service installer reuses the existing service value or generates one on first install. Mark it secret when set and keep the same value for API, worker, and automation.

The deployment uses two accounts: a deploy account for Azure DevOps WinRM/file-copy/install actions and a runtime account for the API and worker Windows Services. The runtime account is also the default Active Directory identity: keep ad.username and ad.bindPassword blank, and SyncFactors binds to AD as the Windows service identity on Windows. The deployment runs scripts/Install-SyncFactorsWindowsPrerequisites.ps1 on the server before service installation. That script creates the install/runtime/log directories, installs or verifies PowerShell 7, creates an optional local runtime account, grants Log on as a service, grants runtime-account access to the deployment paths, and can open the API firewall port. The app bundle is self-contained, so no separate .NET runtime installation is required on the server.

For a manual service-account setup, grant the account:

Permission Reason
Log on as a service on the Windows server Required by Windows Service Control Manager when the API and worker run under a named account.
Modify on C:\SyncFactors and C:\SyncFactors\state Allows the service to read the app/config files and write SQLite runtime state and local logs.
Read access to the HTTPS certificate private key or configured PFX path, when using a service-bound TLS certificate Allows Kestrel to load the HTTPS certificate.
The approved AD delegation for SyncFactors-managed OUs/groups Allows the application to bind to AD as the service identity and apply approved changes with least privilege.

To create and prepare a local account from an elevated PowerShell session:

Set-ExecutionPolicy -Scope Process Bypass
$password = Read-Host 'SyncFactors service password' -AsSecureString

& .\scripts\Install-SyncFactorsWindowsPrerequisites.ps1 `
  -InstallRoot C:\SyncFactors `
  -ServiceAccount sfsvc `
  -ServiceAccountPassword $password `
  -CreateLocalServiceAccount `
  -InstallPowerShell

To create local deploy and runtime accounts with scoped permissions in one pass:

Set-ExecutionPolicy -Scope Process Bypass
$deployPassword = Read-Host 'SyncFactors deploy password' -AsSecureString
$runtimePassword = Read-Host 'SyncFactors runtime password' -AsSecureString

& .\scripts\New-SyncFactorsWindowsServiceAccounts.ps1 `
  -InstallRoot C:\SyncFactors `
  -DeployAccount sfdeploy `
  -DeployAccountPassword $deployPassword `
  -CreateLocalDeployAccount `
  -RuntimeAccount sfsvc `
  -RuntimeAccountPassword $runtimePassword `
  -CreateLocalRuntimeAccount

That script keeps the runtime account out of local administrators. The deploy account is added only to the target server's local Administrators and Remote Management Users groups because Azure DevOps needs administrative rights to copy artifacts, install prerequisites, grant service rights, and create/update Windows Services.

For a pre-created domain account, omit -CreateLocalServiceAccount and pass the domain identity:

Set-ExecutionPolicy -Scope Process Bypass

& .\scripts\Install-SyncFactorsWindowsPrerequisites.ps1 `
  -InstallRoot C:\SyncFactors `
  -ServiceAccount 'CONTOSO\svc-syncfactors' `
  -InstallPowerShell

For pre-created domain deploy and runtime accounts:

Set-ExecutionPolicy -Scope Process Bypass

& .\scripts\New-SyncFactorsWindowsServiceAccounts.ps1 `
  -InstallRoot C:\SyncFactors `
  -DeployAccount 'CONTOSO\svc-syncfactors-deploy' `
  -RuntimeAccount 'CONTOSO\svc-syncfactors-runtime'

Windows services can read configured SyncFactors secrets directly from Windows Credential Manager when the matching environment variable is not set. Store each value under the Windows identity that runs the service, normally svc-syncfactors-runtime, using target names like SyncFactors/SF_AD_SYNC_SF_USERNAME and SyncFactors/SF_AD_SYNC_SF_PASSWORD. To use a different namespace, set the service environment variable SYNCFACTORS_WINDOWS_CREDENTIAL_PREFIX; for example SyncFactors/Production makes the SuccessFactors password target SyncFactors/Production/SF_AD_SYNC_SF_PASSWORD. Do not use credentials stored under your admin/deploy account for service startup; use Start-Process -Credential <runtime-account> -LoadUserProfile or an equivalent privileged process to run the credential script as the runtime account.

Run this while logged on as the runtime service account, or through an approved privileged process that creates credentials in that account profile:

Set-ExecutionPolicy -Scope Process Bypass

& .\scripts\Set-SyncFactorsWindowsCredential.ps1 -VariableName SF_AD_SYNC_AD_SERVER
& .\scripts\Set-SyncFactorsWindowsCredential.ps1 -VariableName SF_AD_SYNC_SF_USERNAME
& .\scripts\Set-SyncFactorsWindowsCredential.ps1 -VariableName SF_AD_SYNC_SF_PASSWORD

To validate a sanitized SuccessFactors export or fixture-style worker document against the expected source contract:

pwsh ./scripts/Validate-SuccessFactorsContract.ps1 -InputPath ./config/mock-successfactors/sample-export.json

You can also fold that into the broader validation command:

pwsh ./scripts/Validate-SyncFactors.ps1 -SuccessFactorsContractPath ./config/mock-successfactors/sample-export.json

Run the checked-in lifecycle simulator sample:

pwsh ./scripts/Test-SyncFactorsLifecycleSimulation.ps1

Run ad hoc real AD end-to-end automation against the mock SuccessFactors stack:

pwsh ./scripts/Bootstrap-SyncFactorsE2EAutomation.ps1
pwsh ./scripts/Run-SyncFactorsE2EAutomation.ps1 `
  -Scenario ./config/automation/sample-real-ad-lifecycle.json `
  -AllowAdReset `
  -IncludeDestructive `
  -StartStack

This drives Mock SuccessFactors, the API run queue, the worker, and configured AD test OUs. -AllowAdReset is required because the runner queues the destructive delete-all reset before scenarios that declare resetAdBeforeScenario.

The bootstrap script creates or updates a local automation Operator account in SQLite, or an Admin account when -Admin is passed for delete-all reset scenarios. It stores SYNCFACTORS_AUTOMATION_USERNAME and SYNCFACTORS_AUTOMATION_PASSWORD in the same secure store as the normal launcher, and switches the worktree auth mode to hybrid so OIDC remains primary while local automation login is available. Restart the API/stack after bootstrapping.

You can also manage those secrets directly in .env.worktree, Windows Credential Manager, or the macOS Keychain:

./scripts/codex/set-macos-keychain-secret.sh SYNCFACTORS_AUTOMATION_USERNAME
./scripts/codex/set-macos-keychain-secret.sh SYNCFACTORS_AUTOMATION_PASSWORD

Run the production-readiness gate against the real local stack:

pwsh ./scripts/Run-SyncFactorsProductionReadiness.ps1 `
  -AllowAdReset `
  -IncludeDestructive `
  -StartStack

The readiness gate runs build, tests, lifecycle simulator coverage, config/AD OU preflight, and the real API/worker/AD scenario suites tagged smoke, identity, and routing. Add -IncludeScale or -IncludeRecovery for the heavier release-gated suites. Reports land under state/runtime/automation-reports/.

Run the focused multi-user sample:

pwsh ./scripts/Test-SyncFactorsLifecycleSimulation.ps1 -Sample multi

Run the failure-behavior sample that validates conflicts and guardrail rebucketing:

pwsh ./scripts/Test-SyncFactorsLifecycleSimulation.ps1 -Sample failure

Run the focused single-worker sample:

pwsh ./scripts/Test-SyncFactorsLifecycleSimulation.ps1 -Sample single

Expected-failure sample that proves assertions are enforced:

pwsh ./scripts/Test-SyncFactorsLifecycleSimulation.ps1 -ExpectedFailure

The bash wrapper does the same thing:

./scripts/test-syncfactors-lifecycle-simulation.sh

The simulator uses:

  • default scenario: config/mock-successfactors/sample-lifecycle-population-scenario.json
  • default fixtures: config/mock-successfactors/sample-lifecycle-multiuser-fixtures.json
  • default report: state/runtime/lifecycle-simulation-population-report.md
  • sibling JSON report: state/runtime/lifecycle-simulation-population-report.json
  • focused multi-user sample: config/mock-successfactors/sample-lifecycle-multiuser-scenario.json
  • focused failure sample: config/mock-successfactors/sample-lifecycle-failure-scenario.json
  • focused single-worker sample: config/mock-successfactors/sample-lifecycle-scenario.json

The checked-in simulator scenarios also roll up into a master test in CI. When desired planner and simulated-lifecycle behavior changes, update the scenario and fixture files first so the master suite remains the executable contract for that simulated behavior. The simulator uses an in-memory directory and applies the requested enabled state directly; it does not model production AD's disabled-first create, transport-dependent password and enablement sequence, licensing-group mutations, or all LDAP failure semantics. Production gateway tests remain the executable contract for those behaviors.

You can also call the CLI directly:

dotnet run --project ./src/SyncFactors.MockSuccessFactors -- simulate-lifecycle --scenario ./config/mock-successfactors/sample-lifecycle-population-scenario.json --fixtures ./config/mock-successfactors/sample-lifecycle-multiuser-fixtures.json --report ./state/runtime/lifecycle-simulation-population-report.md

The API UI now has a small frontend bundle under src/SyncFactors.Api. Install the frontend dependencies once per checkout:

cd ./src/SyncFactors.Api
npm install

Build the browser bundle manually when needed:

npm run build:ui

Or keep it rebuilding while you edit:

npm run watch:ui

The helper scripts under scripts/ and scripts/codex/ are the current supported launch path for the local stack.

Config Model

The current runtime keeps tracked samples and ignored local config under config/.

  • config/sample.mock-successfactors.real-ad.sync-config.json: sample config for mock SuccessFactors plus real Active Directory
  • config/sample.real-successfactors.real-ad.sync-config.json: sample config for real SuccessFactors plus real Active Directory
  • config/sample.empjob-confirmed.mapping-config.json: source sample for the current EmpJob-driven mapping flow
  • config/local*.json: local editable copies created by the worktree bootstrap script when missing

pwsh ./scripts/Update-LocalSyncFactorsConfig.ps1 re-normalizes every tracked local JSON config and .env.worktree against the checked-in samples. Existing local values stay in place for matching keys, sample defaults are added when new keys appear, obsolete local-only keys are removed, and the local files are rewritten into the sample key order and layout.

Sync config resolution currently works like this:

  1. .env.worktree sets SYNCFACTORS_RUN_PROFILE to mock or real
  2. If SYNCFACTORS_CONFIG_PATH is set, that explicit path wins
  3. Otherwise the active profile resolves to config/local.mock-successfactors.real-ad.sync-config.json or config/local.real-successfactors.real-ad.sync-config.json
  4. Mapping config resolves from SYNCFACTORS_MAPPING_CONFIG_PATH, or defaults to config/local.syncfactors.mapping-config.json

config/local.syncfactors.mapping-config.json is created from config/sample.empjob-confirmed.mapping-config.json by the setup and config-sync scripts. config/local.empjob-confirmed.mapping-config.json is kept as a named local copy for comparison and tenant-confirmed mapping work.

.env.worktree is the main per-worktree environment contract. Keep auth, profile selection, ports, and local overrides there rather than in tracked JSON or tracked .codex files.

On Windows, scripts/codex/Load-WorktreeEnv.ps1 checks the worktree-scoped Windows Credential Manager entry for each variable first, then falls back to .env.worktree, then .env.worktree.example, and finally built-in defaults where applicable.

The checked-in .env.worktree.example currently defines:

DOTNET_ENVIRONMENT=Development
ASPNETCORE_ENVIRONMENT=Development
SYNCFACTORS_RUN_PROFILE=mock
SYNCFACTORS_CONFIG_PATH=
SYNCFACTORS_MAPPING_CONFIG_PATH=./config/local.syncfactors.mapping-config.json
SYNCFACTORS_SQLITE_PATH=./state/runtime/syncfactors.db
SYNCFACTORS_SQLITE_PASSWORD=
SYNCFACTORS_API_BIND_HOST=127.0.0.1
SYNCFACTORS_API_PUBLIC_HOST=127.0.0.1
SYNCFACTORS_API_PORT=5087
NUGET_HTTP_CACHE_PATH=./state/nuget/http-cache
NUGET_PACKAGES=./state/nuget/packages
NUGET_PLUGINS_CACHE_PATH=./state/nuget/plugin-cache
SYNCFACTORS_TLS_CERT_PATH=
SYNCFACTORS_TLS_CERT_PASSWORD=
SYNCFACTORS_TLS_CERT_THUMBPRINT=
MOCK_SF_PORT=18080
MOCK_SF_SYNTHETIC_POPULATION_ENABLED=true
MOCK_SF_TARGET_WORKER_COUNT=1000
SYNCFACTORS_KEYCHAIN_SERVICE=syncfactors
SF_AD_SYNC_SF_USERNAME=
SF_AD_SYNC_SF_PASSWORD=
SF_AD_SYNC_SF_CLIENT_ID=mock-client-id
SF_AD_SYNC_SF_CLIENT_SECRET=mock-client-secret
SF_AD_SYNC_AD_SERVER=
SF_AD_SYNC_AD_USERNAME=
SF_AD_SYNC_AD_BIND_PASSWORD=
SF_AD_SYNC_AD_DEFAULT_PASSWORD=

SQLite Encryption

SQLite encryption is enabled by default for Windows Service installs. scripts/Install-SyncFactorsWindowsServices.ps1 sets SYNCFACTORS_SQLITE_PASSWORD in the API and worker service environments by using this order: an explicit -SqlitePassword, an existing installed service environment value, the current process SYNCFACTORS_SQLITE_PASSWORD, or a newly generated password for a fresh/plaintext database. The installer also sets SYNCFACTORS_SECURITY_AUDIT_LOG_PATH to an absolute path under state\runtime so service startup never depends on C:\Windows\system32 as the working directory. Use -DisableSqliteEncryption only for disposable lab installs that intentionally keep a plaintext runtime database.

For non-service launches, set SYNCFACTORS_SQLITE_PASSWORD to enable SQLCipher encryption for the runtime SQLite database. The same value must be present for the API, worker, and automation commands that open the runtime database. The app also recognizes SyncFactors__SqlitePassword, but SYNCFACTORS_SQLITE_PASSWORD is preferred because it is clearly secret material and should come from the OS secret store or service environment rather than tracked JSON.

When the password is configured and the existing database is still plaintext, startup exports the plaintext database to an encrypted SQLCipher copy, replaces the active database file, and leaves an owner-only *.plaintext-<timestamp>.bak rollback copy beside it. Validate startup, then move or securely delete that plaintext backup according to your retention policy. If the password is changed later, the app will not silently rekey an encrypted database; keep the original key available until a deliberate rekey workflow is added. The service installer will stop rather than generate a replacement password when it finds an existing database that does not look plaintext and no current password was supplied or recoverable from the service environment.

.env.worktree.example also carries the Entra/OIDC keys and optional auth session tuning values as commented placeholders until you intentionally enable SSO for that worktree.

On macOS, you can keep sensitive SF_AD_SYNC_* values out of .env.worktree entirely and store them in the login Keychain instead. The launchers fall back to the Keychain service named by SYNCFACTORS_KEYCHAIN_SERVICE when those variables are blank in .env.worktree. To store one:

./scripts/codex/set-macos-keychain-secret.sh SF_AD_SYNC_AD_BIND_PASSWORD

To import the full worktree secret set in one pass:

./scripts/codex/save-worktree-env-to-macos-keychain.sh

That import includes SYNCFACTORS__AUTH__OIDC__CLIENTSECRET when it is present and non-empty in .env.worktree.

To enter selected values interactively and verify each save succeeded:

./scripts/codex/save-worktree-env-to-macos-keychain.sh --interactive SYNCFACTORS__AUTH__OIDC__CLIENTSECRET SF_AD_SYNC_AD_BIND_PASSWORD SF_AD_SYNC_AD_DEFAULT_PASSWORD

On Windows, you can import worktree values into Windows Credential Manager with:

pwsh ./scripts/codex/Save-WorktreeEnvToWindowsCredentialManager.ps1

That import also includes SYNCFACTORS__AUTH__OIDC__CLIENTSECRET when it is present and non-empty in .env.worktree.

Before starting the API on a new admin workstation, run:

pwsh ./scripts/Install-SyncFactorsHttpsCertificate.ps1

That script generates, trusts, and exports the local HTTPS certificate used by the API launcher. By default the launcher binds https://127.0.0.1:<port> and refuses http:// URLs. If SYNCFACTORS_TLS_CERT_PATH and SYNCFACTORS_TLS_CERT_PASSWORD are not set explicitly, the launcher first tries to use a usable certificate from LocalMachine\My that matches SYNCFACTORS_API_PUBLIC_HOST, the configured URL host, or the machine name; when no matching machine certificate is found, it uses the exported certificate from that install step. Set SYNCFACTORS_TLS_CERT_THUMBPRINT to force a specific machine certificate.

If you already have a CA-issued .pfx, use:

pwsh ./scripts/Install-SyncFactorsHttpsCertificateFromPfx.ps1 -PfxPath C:\path\to\syncfactors-api.pfx -PfxPassword '<password>'

That script copies your PFX into the same runtime certificate location used by the launcher, writes the matching password file, and on Windows imports the certificate into the My store. Add -StoreLocation LocalMachine to target the machine store instead of the current user store, or -SkipStoreImport if you only want to configure the app runtime files. When the certificate is in LocalMachine\My and the service account can read its private key, the API can use it by thumbprint without storing the PFX password in the service environment.

For remote UI access from another machine, set these env values in .env.worktree:

SYNCFACTORS_API_BIND_HOST=0.0.0.0
SYNCFACTORS_API_PUBLIC_HOST=<dns-name-or-lan-ip>
SYNCFACTORS_API_PORT=5087

Keep SYNCFACTORS_API_BIND_HOST on the listener address and SYNCFACTORS_API_PUBLIC_HOST on the exact host users browse to. The public host must match the HTTPS certificate and the Entra redirect URIs. The dev certificate from Install-SyncFactorsHttpsCertificate.ps1 is localhost-only, so remote access requires a PFX whose SAN covers <dns-name-or-lan-ip>.

By default, the import helpers skip secure-store variables that are missing or blank in .env.worktree so an existing Keychain or Credential Manager value is left alone. Use --remove-empty-values on macOS or -RemoveEmptyValues on Windows if explicitly blank entries in .env.worktree should delete the corresponding stored credentials.

Set SYNCFACTORS_RUN_PROFILE=mock or real to switch the active SuccessFactors config. Leave SYNCFACTORS_CONFIG_PATH empty for profile-based resolution, or set it only when you want an explicit one-off override.

For a production monitoring deployment that must not expose AD write actions, set:

SyncFactors__Runtime__DryRunOnly=true

This deployment-level override is stronger than sync.realSyncEnabled in the sync JSON. When it is enabled, the API and worker reject live AD writes, scheduled runs queue as dry runs, live-write controls are removed from the UI, and the shared UI shell shows a persistent DRY RUN MODE banner.

To enable local rolling file logs for the SyncFactors API and worker, add these worktree env vars:

SYNCFACTORS_LOCAL_FILE_LOGGING_ENABLED=true
SYNCFACTORS_LOCAL_LOG_DIRECTORY=
SYNCFACTORS_LOCAL_LOG_RETENTION_DAYS=7
SYNCFACTORS_LOCAL_LOG_RETAINED_FILE_COUNT=7
SYNCFACTORS_RUN_FILE_LOGGING_ENABLED=false
SYNCFACTORS_RUN_LOG_RETAINED_FILE_COUNT=200

When enabled, the API writes api-*.log and the worker writes worker-*.log with daily rotation. Process, per-run, and preview logs older than SYNCFACTORS_LOCAL_LOG_RETENTION_DAYS are deleted at service startup and when each daily process log begins; the default is 7 days. SYNCFACTORS_LOCAL_LOG_RETAINED_FILE_COUNT remains a secondary cap on daily process logs. Per-run duplicate logs under runs/<runId>.log are disabled by default to keep disk usage down; set SYNCFACTORS_RUN_FILE_LOGGING_ENABLED=true only when troubleshooting needs a separate run-scoped text log. When per-run logs are enabled, SyncFactors also retains no more than the newest 200 run log files by default. Preview logs are written under preview-logs/ in that same root. Leave SYNCFACTORS_LOCAL_LOG_DIRECTORY blank to use the default runtime log directory.

To send API and worker telemetry to Azure Application Insights, set:

APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=...;IngestionEndpoint=https://...

The API and worker will register Application Insights automatically when that connection string is present. Existing ILogger output continues to work, and local Serilog file logging can stay enabled alongside Application Insights. The older APPINSIGHTS_INSTRUMENTATIONKEY environment variable is also recognized for compatibility, but the connection string is the recommended Azure Monitor configuration.

For Microsoft Entra ID OIDC in local development, add the ASP.NET config-bound environment variables below to .env.worktree once your app registration exists:

SYNCFACTORS__AUTH__MODE=oidc
SYNCFACTORS__AUTH__IDLETIMEOUTMINUTES=480
SYNCFACTORS__AUTH__ABSOLUTESESSIONHOURS=168
SYNCFACTORS__AUTH__OIDC__AUTHORITY=https://login.microsoftonline.com/<tenant-id>/v2.0
SYNCFACTORS__AUTH__OIDC__CLIENTID=<application-client-id>
SYNCFACTORS__AUTH__OIDC__CLIENTSECRET=<client-secret>
SYNCFACTORS__AUTH__OIDC__VIEWERGROUPS__0=<entra-group-object-id>
SYNCFACTORS__AUTH__OIDC__OPERATORGROUPS__0=<entra-group-object-id>
SYNCFACTORS__AUTH__OIDC__ADMINGROUPS__0=<entra-group-object-id>

SYNCFACTORS__AUTH__IDLETIMEOUTMINUTES controls how long an idle browser session can sit before the next refresh requires re-authentication. SYNCFACTORS__AUTH__ABSOLUTESESSIONHOURS caps the total cookie lifetime even with sliding expiration. The app now defaults to an 8-hour idle timeout and a 7-day absolute lifetime, which is usually enough to avoid repeated OIDC prompts during normal use while still forcing periodic re-authentication. If you need stricter or looser settings, the supported ranges are 15-1440 minutes for idle timeout and 8-720 hours for absolute lifetime.

Use tenant-specific authorities, not common, so group and issuer checks stay deterministic for a single-tenant admin tool. Configure the Entra app registration with redirect URIs that match the public HTTPS host and port. For the default local setup:

https://127.0.0.1:5087/signin-oidc
https://127.0.0.1:5087/signout-callback-oidc

If you change SYNCFACTORS_API_PUBLIC_HOST or SYNCFACTORS_API_PORT, the redirect URIs must match that exact HTTPS origin. On macOS, you can keep the OIDC secret out of .env.worktree and store only SYNCFACTORS__AUTH__OIDC__CLIENTSECRET in Keychain with ./scripts/codex/set-macos-keychain-secret.sh SYNCFACTORS__AUTH__OIDC__CLIENTSECRET.

If you want the app registration provisioned for you, use scripts/Configure-EntraOidcAppRegistration.ps1. It connects to Microsoft Graph using your current/default tenant when -TenantId is omitted, or the tenant already configured in .env.worktree when present. It searches for an existing app by supplied object id, supplied or configured client id, display name, then redirect URI; if it finds one, it reconfigures it instead of creating a duplicate. It ensures the enterprise app exists, creates missing security groups for viewer/operator/admin roles, assigns those groups to the enterprise app, writes the resolved auth and OIDC settings back into .env.worktree, and then prints the non-secret values in the terminal summary. The script reuses an OIDC client secret from Windows Credential Manager, macOS Keychain, or the env file when present. Entra does not expose existing secret values, so when no local secret is available the script creates a fresh client secret and stores it in the OS secret store where supported. Set -AuthMode oidc for Entra-only auth or -AuthMode hybrid to keep local break-glass enabled. Use -ApiBindHost 0.0.0.0 -ApiPublicHost <dns-name-or-lan-ip> when you are provisioning a remotely reachable UI. In hybrid mode the script also sets SYNCFACTORS__AUTH__LOCALBREAKGLASS__ENABLED=true, writes the bootstrap admin username, and generates a bootstrap admin password automatically if you do not supply one. On Windows it stores the OIDC client secret and bootstrap admin password in Windows Credential Manager; on macOS it stores those secrets in the login Keychain using SYNCFACTORS_KEYCHAIN_SERVICE from the env file when present, otherwise syncfactors. In those cases the env file keeps the password/secret entries blank so the repo loaders pull them from the secure store. Use -EnvFilePath if you want to target a different env file.

Entra Env Setup

Use the provisioning script when you want the app registration and local auth env configured together. The script updates .env.worktree for you, so you should not need to hand-edit the OIDC values afterward unless you want to override something manually.

End-to-end flow:

  1. Bootstrap the worktree if you have not already:
pwsh ./scripts/codex/setup-worktree.ps1
  1. Run the Entra provisioning script against your tenant. Pick one of the modes below.

OIDC only:

pwsh ./scripts/Configure-EntraOidcAppRegistration.ps1 `
  -TenantId '<tenant-guid>' `
  -AppDisplayName 'SyncFactors Local Dev' `
  -ViewerGroupName 'SyncFactors Viewers' `
  -OperatorGroupName 'SyncFactors Operators' `
  -AdminGroupName 'SyncFactors Admins' `
  -ApiBindHost 0.0.0.0 `
  -ApiPublicHost '<dns-name-or-lan-ip>' `
  -AuthMode oidc `
  -RequireAssignment `
  -InstallModules

Hybrid OIDC plus local break-glass admin:

pwsh ./scripts/Configure-EntraOidcAppRegistration.ps1 `
  -TenantId '<tenant-guid>' `
  -AppDisplayName 'SyncFactors Local Dev' `
  -ViewerGroupName 'SyncFactors Viewers' `
  -OperatorGroupName 'SyncFactors Operators' `
  -AdminGroupName 'SyncFactors Admins' `
  -ApiBindHost 0.0.0.0 `
  -ApiPublicHost '<dns-name-or-lan-ip>' `
  -AuthMode hybrid `
  -BootstrapAdminUsername 'admin' `
  -RequireAssignment `
  -InstallModules

If multiple existing Entra apps match the same redirect URI, target the intended app directly:

pwsh ./scripts/Configure-EntraOidcAppRegistration.ps1 `
  -TenantId '<tenant-guid>' `
  -ClientId '<existing-app-client-id>' `
  -AppDisplayName 'SyncFactors Local Dev' `
  -ViewerGroupName 'SyncFactors Viewers' `
  -OperatorGroupName 'SyncFactors Operators' `
  -AdminGroupName 'SyncFactors Admins' `
  -AuthMode oidc `
  -RequireAssignment
  1. Confirm the env file values. After a successful run, .env.worktree will contain or update these keys:
  • SYNCFACTORS__AUTH__MODE
  • SYNCFACTORS__AUTH__LOCALBREAKGLASS__ENABLED
  • SYNCFACTORS__AUTH__OIDC__AUTHORITY
  • SYNCFACTORS__AUTH__OIDC__CLIENTID
  • SYNCFACTORS__AUTH__OIDC__VIEWERGROUPS__0
  • SYNCFACTORS__AUTH__OIDC__OPERATORGROUPS__0
  • SYNCFACTORS__AUTH__OIDC__ADMINGROUPS__0
  • SYNCFACTORS__AUTH__BOOTSTRAPADMIN__USERNAME in hybrid mode

The script also manages these secrets:

  • SYNCFACTORS__AUTH__OIDC__CLIENTSECRET
  • SYNCFACTORS__AUTH__BOOTSTRAPADMIN__PASSWORD in hybrid mode

On Windows, those secrets are stored in Windows Credential Manager. On macOS, they are stored in the login Keychain. In those cases the .env.worktree entries are intentionally left blank and loaded at runtime by the repo launchers. On other platforms, the script writes the secret values directly into the env file.

  1. Start the app:
pwsh ./scripts/codex/run.ps1 -Service api

Or the full stack:

pwsh ./scripts/codex/run.ps1 -Service stack
  1. Open the login page:
  • https://127.0.0.1:5087/Login
  • Or browse to https://<SYNCFACTORS_API_PUBLIC_HOST>:<SYNCFACTORS_API_PORT>/Login when you configured remote access.

Mode behavior:

  • oidc: Entra sign-in only. The script sets SYNCFACTORS__AUTH__LOCALBREAKGLASS__ENABLED=false.
  • hybrid: Entra sign-in plus local break-glass auth. The script sets SYNCFACTORS__AUTH__LOCALBREAKGLASS__ENABLED=true and provisions the bootstrap admin credentials.

If startup fails because the app is still asking for bootstrap admin credentials while you expected Entra-only auth, check that .env.worktree contains SYNCFACTORS__AUTH__MODE=oidc and SYNCFACTORS__AUTH__LOCALBREAKGLASS__ENABLED=false.

If the app registration already exists, pass either -ApplicationObjectId or -ClientId and the script will reconcile that existing app to the desired state instead of locating one by display name.

Example:

pwsh ./scripts/Configure-EntraOidcAppRegistration.ps1 `
  -AppDisplayName 'SyncFactors Local Dev' `
  -ViewerGroupName 'SyncFactors Viewers' `
  -OperatorGroupName 'SyncFactors Operators' `
  -AdminGroupName 'SyncFactors Admins' `
  -AuthMode oidc `
  -RequireAssignment `
  -InstallModules

Existing app example:

pwsh ./scripts/Configure-EntraOidcAppRegistration.ps1 `
  -TenantId '<tenant-guid>' `
  -ClientId '<existing-app-client-id>' `
  -AppDisplayName 'SyncFactors Local Dev' `
  -ViewerGroupName 'SyncFactors Viewers' `
  -OperatorGroupName 'SyncFactors Operators' `
  -AdminGroupName 'SyncFactors Admins' `
  -RequireAssignment

Hybrid auth example with local break-glass bootstrap admin:

pwsh ./scripts/Configure-EntraOidcAppRegistration.ps1 `
  -TenantId '<tenant-guid>' `
  -ClientId '<existing-app-client-id>' `
  -AppDisplayName 'SyncFactors Local Dev' `
  -ViewerGroupName 'SyncFactors Viewers' `
  -OperatorGroupName 'SyncFactors Operators' `
  -AdminGroupName 'SyncFactors Admins' `
  -AuthMode hybrid `
  -BootstrapAdminUsername 'admin' `
  -RequireAssignment

For Active Directory binds on Windows, the default production deployment leaves ad.username and ad.bindPassword blank so the LDAP connection uses the Windows service identity. Delegate the approved SyncFactors AD permissions to the runtime account, for example svc-syncfactors-runtime, and do not configure separate AD bind credentials unless IAM requires a third account. If you do configure explicit AD bind credentials, set SF_AD_SYNC_AD_USERNAME to a UPN such as svc_syncfactors_adbind@example.local, not a down-level logon name such as EXAMPLE\svc_syncfactors_adbind, or AD may reject the credentials even when the password is correct.

The current AD transport model supports ldaps, starttls, and ldap modes with certificate-validation and signing controls. Keep secure transport as the default for write-capable deployments.

If your primary AD transport is ldaps or starttls and you need an explicit downgrade path for troubleshooting, set ad.transport.allowLdapFallback to true. SyncFactors will try the configured secure transport first and only retry plain LDAP on port 389 when the configured port was the secure default. Leave this disabled unless you intentionally want that behavior.

If you intentionally need create-time account enablement even when the effective transport is plain ldap, set ad.transport.allowCreateEnableWithoutPasswordProvisioning to true. This lets SyncFactors enable the newly created account during the same create operation even though it still cannot set unicodePwd without ldaps or starttls.

For full-sync EmpJob queries, SyncFactors extends configured active source filters with due prehires (emplStatus eq 'I' and startDate on or before today plus sync.enableBeforeStartDays). successFactors.query.inactiveRetentionDays can also extend the source filter to keep recently inactive workers in scope without hand-writing the date cutoff in baseFilter. With the default fields, a config like "baseFilter": "emplStatus in 'A','U'" plus "inactiveRetentionDays": 180 expands to include due prehires and terminated (emplStatus eq 'T') workers whose endDate is within the last 180 days. Override inactiveStatusField, inactiveStatusValues, or inactiveDateField if your tenant uses different fields or status codes.

Queued bulk runs read worker concurrency from sync.maxDegreeOfParallelism. The sample configs default this to 2. Increase it cautiously based on SuccessFactors and AD capacity.

Authentication Modes

The API serves the same operator UI in all auth modes and protects it with cookie auth after sign-in.

By default, the cookie session uses sliding expiration with an 8-hour idle timeout and a 7-day absolute lifetime. Override those with SyncFactors:Auth:IdleTimeoutMinutes and SyncFactors:Auth:AbsoluteSessionHours when your environment needs a different balance between convenience and forced re-authentication.

  • local-break-glass: the default appsettings mode. Local usernames and password hashes live in SQLite, and /Admin/Users manages those local accounts.
  • oidc: enterprise OIDC sign-in only. The login page redirects or offers SSO, and local user management is disabled.
  • hybrid: enterprise OIDC is the primary sign-in path, and local break-glass accounts remain available for emergency access.

On first startup, bootstrap admin credentials are only required when local break-glass auth is enabled and no local users exist yet. In oidc mode, the app skips that bootstrap requirement.

Running The Local Stack

Bootstrap a checkout or worktree first:

pwsh ./scripts/codex/setup-worktree.ps1

That script:

  • creates runtime/report directories when missing
  • creates config/local.mock-successfactors.real-ad.sync-config.json when missing
  • creates config/local.real-successfactors.real-ad.sync-config.json when missing
  • creates config/local.syncfactors.mapping-config.json when missing
  • creates config/local.empjob-confirmed.mapping-config.json when missing
  • creates config/local.codex-run.json when missing
  • creates .env.worktree from .env.worktree.example when missing
  • copies ignored local config from the primary worktree first when available

The intended local loop is:

pwsh ./scripts/codex/setup-worktree.ps1
pwsh ./scripts/codex/run.ps1 -Service mock
pwsh ./scripts/codex/run.ps1 -Service api
pwsh ./scripts/codex/run.ps1 -Service worker

If you only want the operator portal, use:

pwsh ./scripts/codex/run.ps1 -Service ui

There is no separate frontend dev server in the current design. The API serves the bundled UI, and the launcher builds that bundle automatically unless you pass -SkipBuild. -Service ui is effectively an alias for starting the API with the portal assets, and -Service stack brings up that same API as part of the stack.

If you are using Windows Credential Manager, import values before launching services:

pwsh ./scripts/codex/Save-WorktreeEnvToWindowsCredentialManager.ps1

That worktree import command stores secrets for the current Windows identity. For Windows Services, use it only when the current identity is the service runtime account; otherwise use scripts/Set-SyncFactorsWindowsCredential.ps1 from a PowerShell session started as the runtime account.

Or start the profile-aware stack in one command:

pwsh ./scripts/codex/run.ps1 -Service stack

Useful variants:

  • pwsh ./scripts/codex/run.ps1 -Service stack -Profile mock
  • pwsh ./scripts/codex/run.ps1 -Service stack -Profile real
  • pwsh ./scripts/codex/run.ps1 -Service stack -Restart
  • pwsh ./scripts/codex/run.ps1 -Service ui -Profile mock
  • pwsh ./scripts/codex/run.ps1 -Service api -SkipBuild

Before run.ps1 launches services, it checks the tracked local JSON config files and .env.worktree against their checked-in samples. It also validates that .env.worktree.example still contains the launcher's required default keys so new worktrees inherit the full expected env surface. In an interactive terminal it shows the drifted files once and offers to rewrite them in place while preserving current local values for matching fields. In a headless session it fails fast with a remediation message instead of guessing.

If a headless run fails on config/env drift, repair the local files first:

pwsh ./scripts/Update-LocalSyncFactorsConfig.ps1

Then rerun your original pwsh ./scripts/codex/run.ps1 ... command.

For remote UI access, set SYNCFACTORS_API_BIND_HOST=0.0.0.0 and SYNCFACTORS_API_PUBLIC_HOST=<dns-name-or-lan-ip> before launching the API or stack, then browse to https://<SYNCFACTORS_API_PUBLIC_HOST>:<SYNCFACTORS_API_PORT>/Login.

When you run -Service stack, the launched services depend on the active profile:

  • mock: starts the mock SuccessFactors API, the SyncFactors API, and the worker
  • real: starts the SyncFactors API and the worker

At the start of run.ps1, before it loads the rest of the launcher helpers, -Service stack now does git pull --ff-only by default when the current branch has an upstream. If that pull changes HEAD, the launcher restarts itself so the updated scripts are used for the actual run. Toggle that behavior in config/local.codex-run.json:

{
  "git": {
    "pullBeforeStackStart": false
  }
}

-Service ui currently resolves to the same API launch path as -Service api.

The lower-level start scripts remain available if you need to launch individual components directly:

  • scripts/Start-SyncFactorsMockSuccessFactors.ps1
  • scripts/Start-SyncFactorsNextApi.ps1
  • scripts/Start-SyncFactorsWorker.ps1

Codex Worktrees On macOS

Codex workspace settings are local-only for this repository. To prepare a Codex worktree on macOS, run scripts/codex/setup-worktree-macos.sh from the checkout. That macOS wrapper delegates to the shared PowerShell bootstrap script so the setup behavior matches Windows.

The setup is intentionally scoped to the core local dev loop:

  • prepare local config files for the current .NET runtime when missing
  • copy ignored local runtime files from the primary checkout when missing
  • fall back to tracked config/sample*.json files when local config files are still missing
  • create config/local.codex-run.json from the tracked launcher defaults when missing
  • create runtime/report directories used by the API and worker
  • fall back to .env.worktree.example when .env.worktree is still missing

Mock SuccessFactors

Use src/SyncFactors.MockSuccessFactors to run a local SuccessFactors-like API for development.

  • Preferred launcher: scripts/Start-SyncFactorsMockSuccessFactors.ps1
  • Direct run: dotnet run --project src/SyncFactors.MockSuccessFactors --no-launch-profile
  • Default URL: http://127.0.0.1:18080
  • Baseline fixture data: config/mock-successfactors/baseline-fixtures.json
  • Sample import data for sanitization: config/mock-successfactors/sample-export.json

Mock admin endpoints use the actual remote IP address for loopback checks. If a local development harness cannot provide Connection.RemoteIpAddress, opt in to the legacy host-header fallback with MockSuccessFactors:Admin:AllowHostHeaderLoopbackFallback=true; do not use that fallback when the mock is reachable from non-loopback networks.

Generate sanitized fixtures from exported OData payloads with:

dotnet run --project src/SyncFactors.MockSuccessFactors -- \
  generate-fixtures \
  --input config/mock-successfactors/sample-export.json \
  --output /tmp/sanitized-fixtures.json \
  --manifest /tmp/sanitized-fixtures.manifest.json

The mock intentionally supports the query shapes used by the current SyncFactors client: OAuth or Basic auth, PerPerson for preview, EmpJob for the main worker query, $format=json, $filter on personIdExternal or userId, and the current $select and $expand paths used by the client.

If you need to capture a real PerPerson payload before sanitizing it, use:

  • scripts/Export-SfPerPerson.ps1 for OAuth client-credentials auth
  • scripts/Export-SfPerPerson-Basic.ps1 for Basic auth

If you need an admin-safe handoff file, use scripts/Export-SfPerPerson-Sanitized.ps1. It fetches the response, sanitizes it in memory, and writes only the sanitized JSON to disk. Add -AliasOrgValues if company, department, or location labels should also be anonymized, and -KeepPersonIdExternal if you need to preserve the source worker ID.

If your tenant rejects one of the optional fields in the hard-coded query, all three export scripts support -ExcludeSelectPath and -ExcludeExpandPath. For example, to skip business unit:

-ExcludeSelectPath "employmentNav/jobInfoNav/businessUnitNav/businessUnit" `
-ExcludeExpandPath "employmentNav/jobInfoNav/businessUnitNav"

If you want to probe the broader employee header set for one worker without sanitizing the output, use scripts/Export-SfEmployeeHeaderProfile.ps1. It wraps the same auto-retrying query logic with -IncludeHeaderProfile -SkipSanitization already enabled so you can see which fields your tenant actually exposes.

If you want to discover likely field mappings from tenant metadata before querying a worker, use scripts/Get-SfMetadataFieldCandidates.ps1. It downloads /$metadata, searches for the employee headers we discussed, and writes candidate entities and OData paths to JSON.

If you want to query one worker using the tighter, metadata-derived employee field set, use scripts/Export-SfEmployeeMetadataProfile.ps1. It targets the strongest candidate paths from the metadata analysis, skips sanitization, and keeps the same auto-retry behavior for unsupported tenant fields.

CI And Security Gates

GitHub Actions currently provides:

  • test.yml: .NET restore/build/test, frontend dependency install, frontend tests, frontend bundle build, lifecycle simulation master coverage, SBOM generation, and SBOM provenance attestation when permitted
  • security.yml: SonarCloud quality gate, Semgrep SAST, private-value denylist scan, Gitleaks, and Trivy repository scan
  • codeql.yml: CodeQL analysis for C# and JavaScript/TypeScript
  • dependency-review.yml: pull-request dependency review
  • release.yml: Windows x64 self-contained release bundle publishing

Main branch merge policy

auto-merge.yml can only enable auto-merge for a same-repository PR opened by dependabot[bot], or for a PR carrying the automerge:approved label. A maintainer must apply that label only after review; contributors without repository write access must not be allowed to apply it. Auto-merge remains subject to the required checks below.

Apply the following settings to the main branch protection rule (or an equivalent repository ruleset). These are GitHub repository settings and are intentionally not changed by this repository:

  • Require a pull request before merging, with at least one approving review; dismiss stale approvals when new commits are pushed; require conversation resolution; and restrict bypass permissions to repository administrators.
  • Require status checks to pass and require the branch to be up to date before merging. Select these exact check names: dotnet, GitHub Workflow Security Policy, Semgrep Security SAST, Gitleaks Secret Scan, Trivy Repository Scan, Analyze (csharp, none), Analyze (javascript-typescript, none), and Dependency Review.
  • Enable Allow auto-merge. Do not make Release a required PR check: test.yml, security.yml, and release.yml run after the merge on the resulting main push, so the release workflow cannot be a pre-merge check.
  • Block direct pushes to main except for the intended repository administrators and automation, and require the same rules for administrators.

The Azure DevOps deployment pipeline is separate from GitHub Actions. azure-pipelines.deploy.yml builds and tests the same solution, creates the Windows bundle, and deploys only when deployment variables are configured.

About

.NET automation for syncing SAP SuccessFactors worker data into on-premises Active Directory.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages