Public/New-IdleLifecycleRequestObject.ps1

function New-IdleLifecycleRequestObject {
    <#
    .SYNOPSIS
    Creates a lifecycle request object (core factory).

    .DESCRIPTION
    Constructs and returns an IdleLifecycleRequest domain object representing business intent
    (e.g. Joiner/Mover/Leaver). This is the core factory function used by the IdLE module wrapper.
    
    The function validates that no ScriptBlocks are present in the input data (IdentityKeys,
    DesiredState, Changes) to enforce the data-only configuration principle. Input hashtables
    are cloned to prevent external mutation after object creation.
    
    CorrelationId is preserved if provided; otherwise, the IdleLifecycleRequest class generates
    a new GUID. Actor is optional and not required by the core engine.

    .PARAMETER LifecycleEvent
    The lifecycle event name (e.g. Joiner, Mover, Leaver). This is a required string that
    identifies the type of lifecycle operation being requested.

    .PARAMETER CorrelationId
    Optional correlation identifier for audit and event correlation. If omitted, a GUID is
    automatically generated by the IdleLifecycleRequest constructor.

    .PARAMETER Actor
    Optional actor claim identifying who initiated the request. Not required by the core
    engine in V1 but may be used for audit logging or workflow-specific logic.

    .PARAMETER IdentityKeys
    A hashtable of system-neutral identity keys (e.g. @{ EmployeeId = '12345'; UPN = 'user@contoso.com' }).
    Defaults to an empty hashtable if not provided. Must not contain ScriptBlocks.

    .PARAMETER DesiredState
    A hashtable describing the desired state for the identity (attributes, entitlements, etc.).
    Defaults to an empty hashtable if not provided. Must not contain ScriptBlocks.

    .PARAMETER Changes
    Optional hashtable describing changes (typically used for Mover lifecycle events to indicate
    what changed from the previous state). Remains $null when omitted. Must not contain ScriptBlocks.

    .EXAMPLE
    $request = New-IdleLifecycleRequestObject -LifecycleEvent 'Joiner'

    Creates a minimal Joiner request with auto-generated CorrelationId and empty IdentityKeys/DesiredState.

    .EXAMPLE
    $request = New-IdleLifecycleRequestObject -LifecycleEvent 'Joiner' -CorrelationId (New-Guid).Guid -IdentityKeys @{ EmployeeId = '12345' } -DesiredState @{ Department = 'Engineering'; MailNickname = 'jdoe'; Title = 'Engineer' }

    Creates a Joiner request with specific identity keys and desired state attributes for a typical onboarding workflow.

    .EXAMPLE
    $request = New-IdleLifecycleRequestObject -LifecycleEvent 'Mover' -IdentityKeys @{ UPN = 'user@contoso.com' } -Changes @{ Department = 'Sales' } -Actor 'admin@contoso.com'

    Creates a Mover request with identity keys, changes, and actor information for a department transfer workflow.

    .OUTPUTS
    IdleLifecycleRequest

    .NOTES
    Security Considerations:
    - Input data must be data-only (no ScriptBlocks or executable objects). The function
      validates this constraint and throws if violated.
    - Do not embed secrets in IdentityKeys, DesiredState, or Changes. Use the AuthSessionBroker
      pattern for credential/token management.
    - Sensitive data in request objects may be logged or emitted in events. Rely on redaction
      boundaries defined in the engine's event sink and logging layers.
    
    This is a core engine function. For the user-facing API, use New-IdleLifecycleRequest from
    the IdLE module, which delegates to this function.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $LifecycleEvent,

        [Parameter()]
        [string] $CorrelationId,

        [Parameter()]
        [string] $Actor,

        [Parameter()]
        [hashtable] $IdentityKeys = @{},

        [Parameter()]
        [hashtable] $DesiredState = @{},

        [Parameter()]
        [hashtable] $Changes
    )

    # Validate that no ScriptBlocks are present in the input data
    Assert-IdleNoScriptBlock -InputObject $IdentityKeys -Path 'IdentityKeys'
    Assert-IdleNoScriptBlock -InputObject $DesiredState -Path 'DesiredState'
    Assert-IdleNoScriptBlock -InputObject $Changes      -Path 'Changes'

    # Clone hashtables to avoid external mutation after object creation
    # shallow clone is sufficient as we have already validated no ScriptBlocks are present
    $IdentityKeys = if ($null -eq $IdentityKeys) { @{} } else { $IdentityKeys.Clone() }
    $DesiredState = if ($null -eq $DesiredState) { @{} } else { $DesiredState.Clone() }
    $Changes = if ($null -eq $Changes) { $null } else { $Changes.Clone() }

    # Construct and return the core domain object defined in Private/IdleLifecycleRequest.ps1
    return [IdleLifecycleRequest]::new(
        $LifecycleEvent,
        $IdentityKeys,
        $DesiredState,
        $Changes,
        $CorrelationId,
        $Actor
    )
}