Plugins/MijnHost.ps1

function Get-CurrentPluginType { 'dns-01' }

function Add-DnsTxt {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]$RecordName,

        [Parameter(Mandatory, Position = 1)]
        [string]$TxtValue,

        [Parameter(Mandatory, Position = 2)]
        [securestring]$MijnHostApiKey,

        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    $apiKey = [PSCredential]::new('a', $MijnHostApiKey).GetNetworkCredential().Password
    $restParams = Get-MijnHostRestParams -ApiKey $apiKey

    $zone = Find-MijnHostZone -RecordName $RecordName -RestParams $restParams
    if (-not $zone) { throw "Unable to find mijn.host zone for $RecordName" }

    # API requires a trailing dot on record names
    $fqdn = "$RecordName."

    # The mijn.host API (PowerDNS) expects TXT values without surrounding quotes.
    # Posh-ACME passes dns-persist-01 TxtValues wrapped in double quotes, so strip them.
    $normalizedValue = $TxtValue -replace '^"(.*)"$', '$1'

    # Fetch current records and check for an existing match (idempotent)
    $current = Invoke-MijnHostDnsGet -Zone $zone -RestParams $restParams
    $exists = $current | Where-Object { $_.type -eq 'TXT' -and $_.name -eq $fqdn -and $_.value -eq $normalizedValue }

    if ($exists) {
        Write-Debug "Record $RecordName already contains $normalizedValue. Nothing to do."
        return
    }

    Write-Verbose "Adding TXT record $fqdn on zone $zone"
    $newRecords = [array]$current + [PSCustomObject]@{ type = 'TXT'; name = $fqdn; value = $normalizedValue; ttl = 900 }
    Invoke-MijnHostDnsPut -Zone $zone -Records $newRecords -RestParams $restParams

    <#
    .SYNOPSIS
        Add a DNS TXT record to mijn.host.
    .DESCRIPTION
        Uses the mijn.host API to add a DNS TXT record for ACME DNS-01 challenges.
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
    .PARAMETER TxtValue
        The value of the TXT record.
    .PARAMETER MijnHostApiKey
        The API key for your mijn.host account as a SecureString.
    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
    .EXAMPLE
        $key = Read-Host 'API Key' -AsSecureString
        Add-DnsTxt '_acme-challenge.example.com' 'txt-value' -MijnHostApiKey $key
    #>

}

function Remove-DnsTxt {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]$RecordName,

        [Parameter(Mandatory, Position = 1)]
        [string]$TxtValue,

        [Parameter(Mandatory, Position = 2)]
        [securestring]$MijnHostApiKey,

        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )

    $apiKey = [PSCredential]::new('a', $MijnHostApiKey).GetNetworkCredential().Password
    $restParams = Get-MijnHostRestParams -ApiKey $apiKey

    $zone = Find-MijnHostZone -RecordName $RecordName -RestParams $restParams
    if (-not $zone) { throw "Unable to find mijn.host zone for $RecordName" }

    $fqdn = "$RecordName."
    $normalizedValue = $TxtValue -replace '^"(.*)"$', '$1'

    $current = Invoke-MijnHostDnsGet -Zone $zone -RestParams $restParams
    $remaining = $current | Where-Object { -not ($_.type -eq 'TXT' -and $_.name -eq $fqdn -and $_.value -eq $normalizedValue) }

    if ($current.Count -eq @($remaining).Count) {
        Write-Debug "Record $RecordName with value $normalizedValue does not exist. Nothing to do."
        return
    }

    Write-Verbose "Removing TXT record $fqdn from zone $zone"
    Invoke-MijnHostDnsPut -Zone $zone -Records $remaining -RestParams $restParams

    <#
    .SYNOPSIS
        Remove a DNS TXT record from mijn.host.
    .DESCRIPTION
        Uses the mijn.host API to remove a DNS TXT record after ACME DNS-01 challenge completion.
    .PARAMETER RecordName
        The fully qualified name of the TXT record.
    .PARAMETER TxtValue
        The value of the TXT record.
    .PARAMETER MijnHostApiKey
        The API key for your mijn.host account as a SecureString.
    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
    .EXAMPLE
        $key = Read-Host 'API Key' -AsSecureString
        Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' -MijnHostApiKey $key
    #>

}

function Save-DnsTxt {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromRemainingArguments)]
        $ExtraParams
    )
    <#
    .SYNOPSIS
        Not required for mijn.host.
    .DESCRIPTION
        mijn.host applies DNS changes immediately. No commit step is needed.
    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
    #>

}

############################
# Helper Functions
############################

# API docs: https://mijn.host/api/doc

function Get-MijnHostRestParams {
    param (
        [string]$ApiKey
    )
    return @{
        Headers     = @{
            'API-Key' = $ApiKey
            'Accept'  = 'application/json'
        }
        ContentType = 'application/json'
        ErrorAction = 'Stop'
    }
}

function Invoke-MijnHostDnsGet {
    param (
        [string]$Zone,

        [hashtable]$RestParams
    )
    $url = "https://mijn.host/api/v2/domains/$Zone/dns"
    Write-Debug "GET $url"
    $response = Invoke-RestMethod $url @RestParams -Method Get @script:UseBasic
    return $response.data.records
}

function Invoke-MijnHostDnsPut {
    param (
        [string]$Zone,

        $Records,

        [hashtable]$RestParams
    )
    $url = "https://mijn.host/api/v2/domains/$Zone/dns"
    $body = @{ records = @($Records) } | ConvertTo-Json -Depth 10 -Compress
    Write-Debug "PUT $url`n$body"
    Invoke-RestMethod $url @RestParams -Method Put -Body $body @script:UseBasic | Out-Null
}

function Find-MijnHostZone {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]$RecordName,

        [Parameter(Mandatory, Position = 1)]
        [hashtable]$RestParams
    )

    if (!$script:MijnHostZoneCache) { $script:MijnHostZoneCache = @{} }

    if ($script:MijnHostZoneCache.ContainsKey($RecordName)) {
        return $script:MijnHostZoneCache.$RecordName
    }

    # Fetch the list of zones the API key can manage, then find the longest
    # suffix of RecordName that matches a known zone. Using the domains list
    # avoids per-zone probe requests and correctly handles the domain apex case
    # (where the record name itself is the zone, e.g. when using -DnsAlias).
    $url = "https://mijn.host/api/v2/domains"
    Write-Debug "GET $url"
    $response = Invoke-RestMethod $url @RestParams -Method Get @script:UseBasic
    $zones = $response.data.domains | Select-Object -ExpandProperty domain

    $pieces = $RecordName.Split('.')
    for ($i = 0; $i -lt $pieces.Count; $i++) {
        $zone = ($pieces[$i..($pieces.Count - 1)]) -join '.'
        if ($zone -in $zones) {
            Write-Debug "Matched zone: $zone"
            $script:MijnHostZoneCache.$RecordName = $zone
            return $zone
        }
    }

    return $null
}