Apify Secret Inputs
This guide explains how Apify’s secret input feature works and provides detailed implementation pointers for the Elixir language.
Table of Contents
Section titled “Table of Contents”- Overview
- How Secret Inputs Work
- Encryption Mechanism
- Decryption Process
- Environment Variables
- Data Formats
- Elixir Implementation Guide
- Testing
- References
Overview
Section titled “Overview”Apify’s secret input feature allows you to mark specific input fields as sensitive. These fields are automatically encrypted before being stored and can only be decrypted within the Actor’s execution environment.
Key Features
Section titled “Key Features”- Dual Encryption: Uses PGP-like dual encryption (RSA + AES)
- Type Support: Works with
string,object, andarrayinput types - Isolation: Each user/Actor combination has unique RSA keys
- Security: Decryption keys only available during Actor execution via environment variables
How Secret Inputs Work
Section titled “How Secret Inputs Work”1. Input Schema Configuration
Section titled “1. Input Schema Configuration”Mark a field as secret in your Actor’s input schema:
{ "properties": { "password": { "title": "Password", "type": "string", "description": "A secret, encrypted input field", "editor": "textfield", "isSecret": true } }}2. Encryption Flow (Apify Platform Side)
Section titled “2. Encryption Flow (Apify Platform Side)”When you save Actor input through the Apify Console or API:
- Detect Secret Fields: Apify identifies fields marked with
isSecret: true - Encrypt Values: Each secret value is encrypted using:
- Random AES-256-GCM key and IV
- Actor/user-specific RSA-2048 public key
- Store Encrypted: Encrypted values saved to key-value store in format:
ENCRYPTED_VALUE:<encrypted_password>:<encrypted_data>
3. Decryption Flow (Actor Side)
Section titled “3. Decryption Flow (Actor Side)”When your Actor calls getInput():
- Load Input: Read from key-value store (contains encrypted values)
- Check Environment: Look for decryption keys in environment variables
- Decrypt Fields: For each encrypted field:
- Parse the encrypted value format
- RSA decrypt the password buffer
- AES-GCM decrypt the actual value
- Return Decrypted: Return input with decrypted secret fields
Encryption Mechanism
Section titled “Encryption Mechanism”Algorithm Details
Section titled “Algorithm Details”Symmetric Encryption (Data):
- Algorithm: AES-256-GCM
- Key Length: 32 bytes (256 bits)
- IV Length: 16 bytes (128 bits)
- Auth Tag: 16 bytes (appended to ciphertext)
Asymmetric Encryption (Key):
- Algorithm: RSA-2048
- Padding: OAEP (Optimal Asymmetric Encryption Padding)
- Purpose: Encrypts the AES key + IV combination
Encryption Process (For Reference)
Section titled “Encryption Process (For Reference)”1. Generate random 32-byte AES key2. Generate random 16-byte IV3. Encrypt data with AES-256-GCM: - Result: ciphertext + 16-byte auth tag4. Concatenate key + IV (48 bytes total)5. RSA-encrypt the 48-byte password buffer6. Base64 encode both encrypted password and encrypted value7. Format as: ENCRYPTED_VALUE:<base64_password>:<base64_value>Decryption Process
Section titled “Decryption Process”This is what you need to implement in Elixir.
Step-by-Step Decryption
Section titled “Step-by-Step Decryption”Step 1: Parse Encrypted Value Format
Section titled “Step 1: Parse Encrypted Value Format”Format Pattern:
^(ENCRYPTED_VALUE|ENCRYPTED_JSON):(?:([a-f0-9]{10}):)?([A-Za-z0-9+/=]+):([A-Za-z0-9+/=]+)$Components:
- Prefix:
ENCRYPTED_VALUE(strings) orENCRYPTED_JSON(objects/arrays) - Schema Hash (optional): 10-character hex string (for objects/arrays)
- Encrypted Password: Base64-encoded RSA-encrypted AES key + IV
- Encrypted Value: Base64-encoded AES-GCM-encrypted data + auth tag
Step 2: RSA Decrypt Password Buffer
Section titled “Step 2: RSA Decrypt Password Buffer”Input: Base64-encoded encrypted password
Process:
- Base64 decode → get encrypted password bytes
- RSA-2048 decrypt using private key (with passphrase)
- Result: 48-byte buffer
- First 32 bytes: AES key
- Last 16 bytes: IV
Validation:
- Password buffer must be exactly 48 bytes
- If not, decryption failed
Step 3: AES-GCM Decrypt Value
Section titled “Step 3: AES-GCM Decrypt Value”Input: Base64-encoded encrypted value
Process:
- Base64 decode → get encrypted value bytes
- Split into:
- Encrypted data: all bytes except last 16
- Auth tag: last 16 bytes
- Decrypt using AES-256-GCM:
- Key: from Step 2 (32 bytes)
- IV: from Step 2 (16 bytes)
- Auth tag: from this step (16 bytes)
- Ciphertext: from this step (remaining bytes)
Step 4: Parse Result Based on Prefix
Section titled “Step 4: Parse Result Based on Prefix”- If
ENCRYPTED_VALUE: Return decrypted string as-is - If
ENCRYPTED_JSON: Parse decrypted string as JSON
Environment Variables
Section titled “Environment Variables”Required Environment Variables
Section titled “Required Environment Variables”Your Elixir Actor needs to read these environment variables:
| Variable Name | Description | Example |
|---|---|---|
APIFY_INPUT_SECRETS_PRIVATE_KEY_FILE | Base64-encoded RSA private key (encrypted with passphrase) | LS0tLS1CRUdJTiBSU0E... |
APIFY_INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE | Passphrase to decrypt the private key | pwd1234 |
When These Variables Are Available
Section titled “When These Variables Are Available”- During Actor execution: Always available
- Local development: Not available (secrets won’t be decrypted)
- Outside Actor run: Not available (security measure)
Data Formats
Section titled “Data Formats”Encrypted String Field
Section titled “Encrypted String Field”ENCRYPTED_VALUE:Hw/uqRMRNHmxXYYDJCyaQX6xcwUnVYQnH4fWIlKZL...:Kj8d9f...Encrypted JSON Field (Object/Array)
Section titled “Encrypted JSON Field (Object/Array)”ENCRYPTED_JSON:a1b2c3d4e5:Hw/uqRMRNHmxXYYDJCyaQX6xcwUnVYQnH4fWIlKZL...:Kj8d9f...The schema hash (a1b2c3d4e5) is used to verify the field schema matches.
Input Object Example
Section titled “Input Object Example”Before Encryption:
{ "username": "john_doe", "password": "mySecretPassword123", "apiCredentials": { "key": "abc", "secret": "xyz" }}After Encryption:
{ "username": "john_doe", "password": "ENCRYPTED_VALUE:Hw/uqRM...:Kj8d9f...", "apiCredentials": "ENCRYPTED_JSON:a1b2c3d4e5:Hw/uqRM...:Kj8d9f..."}Elixir Implementation Guide
Section titled “Elixir Implementation Guide”Required Dependencies
Section titled “Required Dependencies”Add to your mix.exs:
defp deps do [ {:jason, "~> 1.4"} # For JSON parsing ]endErlang’s :crypto and :public_key modules are built-in.
Implementation Structure
Section titled “Implementation Structure”1. Configuration Module
Section titled “1. Configuration Module”defmodule Apify.Config do @moduledoc "Read Apify environment variables"
def get_private_key do System.get_env("APIFY_INPUT_SECRETS_PRIVATE_KEY_FILE") end
def get_passphrase do System.get_env("APIFY_INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE") end
def has_decryption_keys? do get_private_key() != nil and get_passphrase() != nil endend2. RSA Decryption Module
Section titled “2. RSA Decryption Module”defmodule Apify.Crypto.RSA do @moduledoc "RSA decryption using Erlang :public_key"
@doc """ Decrypt a password buffer using RSA private key
## Parameters - encrypted_password_b64: Base64-encoded encrypted password - private_key_b64: Base64-encoded encrypted private key - passphrase: Passphrase for the private key
## Returns - {:ok, <<32-byte key, 16-byte iv>>} on success - {:error, reason} on failure """ def decrypt_password(encrypted_password_b64, private_key_b64, passphrase) do with {:ok, encrypted_password} <- Base.decode64(encrypted_password_b64), {:ok, private_key_pem} <- Base.decode64(private_key_b64), {:ok, private_key} <- decode_private_key(private_key_pem, passphrase), {:ok, decrypted} <- rsa_decrypt(encrypted_password, private_key) do if byte_size(decrypted) == 48 do {:ok, decrypted} else {:error, "Invalid password length: expected 48 bytes, got #{byte_size(decrypted)}"} end end end
defp decode_private_key(pem_data, passphrase) do try do # Decode PEM [pem_entry] = :public_key.pem_decode(pem_data)
# Decrypt private key with passphrase private_key = :public_key.pem_entry_decode(pem_entry, passphrase) {:ok, private_key} rescue e -> {:error, "Failed to decode private key: #{inspect(e)}"} end end
defp rsa_decrypt(encrypted_data, private_key) do try do # RSA decryption with OAEP padding decrypted = :public_key.decrypt_private( encrypted_data, private_key, # Note: You may need to adjust padding based on how Node.js crypto does it # Common options: :rsa_pkcs1_oaep_padding, :rsa_pkcs1_padding [{:rsa_pad, :rsa_pkcs1_oaep_padding}] ) {:ok, decrypted} rescue e -> {:error, "RSA decryption failed: #{inspect(e)}"} end endend3. AES-GCM Decryption Module
Section titled “3. AES-GCM Decryption Module”defmodule Apify.Crypto.AES do @moduledoc "AES-256-GCM decryption"
@key_length 32 @iv_length 16 @auth_tag_length 16
@doc """ Decrypt value using AES-256-GCM
## Parameters - encrypted_value_b64: Base64-encoded encrypted value (ciphertext + auth tag) - password_buffer: 48-byte buffer containing key (32) + iv (16)
## Returns - {:ok, decrypted_string} on success - {:error, reason} on failure """ def decrypt_value(encrypted_value_b64, password_buffer) do with {:ok, encrypted_value} <- Base.decode64(encrypted_value_b64), {:ok, {key, iv, ciphertext, auth_tag}} <- parse_encrypted_value(encrypted_value, password_buffer), {:ok, decrypted} <- aes_gcm_decrypt(ciphertext, key, iv, auth_tag) do {:ok, decrypted} end end
defp parse_encrypted_value(encrypted_value, password_buffer) do if byte_size(encrypted_value) < @auth_tag_length do {:error, "Encrypted value too short"} else # Split password buffer into key and IV <<key::binary-size(@key_length), iv::binary-size(@iv_length)>> = password_buffer
# Split encrypted value into ciphertext and auth tag ciphertext_size = byte_size(encrypted_value) - @auth_tag_length <<ciphertext::binary-size(ciphertext_size), auth_tag::binary-size(@auth_tag_length)>> = encrypted_value
{:ok, {key, iv, ciphertext, auth_tag}} end end
defp aes_gcm_decrypt(ciphertext, key, iv, auth_tag) do try do # AES-256-GCM decryption using Erlang :crypto decrypted = :crypto.crypto_one_time_aead( :aes_256_gcm, key, iv, ciphertext, "", # Additional authenticated data (AAD) - empty in this case auth_tag, false # false = decrypt ) {:ok, decrypted} rescue e -> {:error, "AES-GCM decryption failed: #{inspect(e)}"} end endend4. Secret Input Decryption Module
Section titled “4. Secret Input Decryption Module”defmodule Apify.SecretInput do @moduledoc "Decrypt Apify secret input fields"
@encrypted_value_prefix "ENCRYPTED_VALUE" @encrypted_json_prefix "ENCRYPTED_JSON" @encrypted_regex ~r/^(ENCRYPTED_VALUE|ENCRYPTED_JSON):(?:([a-f0-9]{10}):)?([A-Za-z0-9+/=]+):([A-Za-z0-9+/=]+)$/
@doc """ Decrypt all secret fields in the input object
## Parameters - input: Map containing potentially encrypted fields - private_key_b64: Base64-encoded private key - passphrase: Passphrase for private key
## Returns - Map with decrypted secret fields """ def decrypt_secrets(input, private_key_b64, passphrase) when is_map(input) do Enum.reduce(input, %{}, fn {key, value}, acc -> decrypted_value = decrypt_if_encrypted(value, private_key_b64, passphrase) Map.put(acc, key, decrypted_value) end) end
def decrypt_secrets(input, _, _), do: input
defp decrypt_if_encrypted(value, private_key_b64, passphrase) when is_binary(value) do case Regex.run(@encrypted_regex, value) do [_, prefix, _schema_hash, encrypted_password, encrypted_value] -> decrypt_encrypted_field(prefix, encrypted_password, encrypted_value, private_key_b64, passphrase)
nil -> # Not encrypted, return as-is value end end
defp decrypt_if_encrypted(value, _, _), do: value
defp decrypt_encrypted_field(prefix, encrypted_password, encrypted_value, private_key_b64, passphrase) do with {:ok, password_buffer} <- Apify.Crypto.RSA.decrypt_password( encrypted_password, private_key_b64, passphrase ), {:ok, decrypted_string} <- Apify.Crypto.AES.decrypt_value(encrypted_value, password_buffer) do # Parse based on prefix case prefix do @encrypted_value_prefix -> # Return as string {:ok, decrypted_string}
@encrypted_json_prefix -> # Parse as JSON case Jason.decode(decrypted_string) do {:ok, json_value} -> {:ok, json_value} {:error, _} = error -> error end end else {:error, reason} -> # Log error and return original encrypted value IO.warn("Failed to decrypt secret field: #{reason}") {:error, reason} end end
@doc """ Check if a value is encrypted """ def encrypted?(value) when is_binary(value) do Regex.match?(@encrypted_regex, value) end
def encrypted?(_), do: falseend5. Main Input Reader
Section titled “5. Main Input Reader”defmodule Apify.Input do @moduledoc "Read and decrypt Apify Actor input"
@doc """ Get Actor input, automatically decrypting secret fields if keys are available
## Returns - {:ok, input_map} on success - {:error, reason} on failure """ def get_input do # Read input from key-value store (you need to implement this) with {:ok, raw_input} <- read_raw_input() do # Check if we have decryption keys if Apify.Config.has_decryption_keys?() do decrypt_input(raw_input) else # No keys available, return raw input {:ok, raw_input} end end end
defp decrypt_input(raw_input) when is_map(raw_input) do private_key = Apify.Config.get_private_key() passphrase = Apify.Config.get_passphrase()
decrypted = Apify.SecretInput.decrypt_secrets( raw_input, private_key, passphrase )
{:ok, decrypted} end
defp decrypt_input(raw_input), do: {:ok, raw_input}
defp read_raw_input do # TODO: Implement reading from Apify key-value store # This will depend on how you access the KVS in your Elixir implementation # Example: # - Read from file: /apify_storage/key_value_stores/default/INPUT.json # - Or use Apify API client {:ok, %{}} endendUsage Example
Section titled “Usage Example”# In your Actor's main moduledefmodule MyActor do def run do case Apify.Input.get_input() do {:ok, input} -> IO.inspect(input, label: "Decrypted input")
# Access secret fields normally password = input["password"] IO.puts("Password: #{password}")
{:error, reason} -> IO.puts("Failed to get input: #{reason}") end endendTesting
Section titled “Testing”Test Data from SDK
Section titled “Test Data from SDK”The JavaScript SDK test suite includes test keys you can use:
Public Key (for reference):
# Base64-encoded RSA public keypublic_key_b64 = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF0dis3NlNXbklhOFFKWC94RUQxRQpYdnBBQmE3ajBnQnVYenJNUU5adjhtTW1RU0t2VUF0TmpOL2xacUZpQ0haZUQxU2VDcGV1MnFHTm5XbGRxNkhUCnh5cXJpTVZEbFNKaFBNT09QSENISVNVdFI4Tk5lR1Y1MU0wYkxJcENabHcyTU9GUjdqdENWejVqZFRpZ1NvYTIKQWxrRUlRZWQ4UVlDKzk1aGJoOHk5bGcwQ0JxdEdWN1FvMFZQR2xKQ0hGaWNuaWxLVFFZay9MZzkwWVFnUElPbwozbUppeFl5bWFGNmlMZTVXNzg1M0VHWUVFVWdlWmNaZFNjaGVBMEdBMGpRSFVTdnYvMEZjay9adkZNZURJOTVsCmJVQ0JoQjFDbFg4OG4wZUhzUmdWZE5vK0NLMDI4T2IvZTZTK1JLK09VaHlFRVdPTi90alVMdGhJdTJkQWtGcmkKOFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="Private Key File (base64-encoded, encrypted):
# This is the base64-encoded PEM file (which itself is encrypted)private_key_file_b64 = "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpQcm9jLVR5cGU6IDQsRU5DUllQVEVECkRFSy1JbmZvOiBERVMtRURFMy1DQkMsNTM1QURERjIzNUQ4QkFGOQoKMXFWUzl0S0FhdkVhVUVFMktESnpjM3plMk1lZkc1dmVEd2o1UVJ0ZkRaMXdWNS9VZmIvcU5sVThTSjlNaGhKaQp6RFdrWExueUUzSW0vcEtITVZkS0czYWZkcFRtcis2TmtidXptd0dVMk0vSWpzRjRJZlpad0lGbGJoY09jUnp4CmZmWVIvTlVyaHNrS1RpNGhGV0lBUDlLb3Z6VDhPSzNZY3h6eVZQWUxYNGVWbWt3UmZzeWkwUU5Xb0tGT3d0ZC8KNm9HYzFnd2piRjI5ZDNnUThZQjFGWmRLa1AyMTJGbkt1cTIrUWgvbE1zTUZrTHlTQTRLTGJ3ZG1RSXExbE1QUwpjbUNtZnppV3J1MlBtNEZoM0dmWlQyaE1JWHlIRFdEVzlDTkxKaERodExOZ2RRamFBUFpVT1E4V2hwSkE5MS9vCjJLZzZ3MDd5Z2RCcVd5dTZrc0pXcjNpZ1JpUEJ5QmVNWEpEZU5HY3NhaUZ3Q2c5eFlja1VORXR3NS90WlRsTjIKSEdZV0NpVU5Ed0F2WllMUHR1SHpIOFRFMGxsZm5HR0VuVC9QQlp1UHV4andlZlRleE1mdzFpbGJRU3lkcy9HMgpOOUlKKzkydms0N0ZXR2NOdGh1Q3lCbklva0NpZ0c1ZlBlV2IwQTdpdjk0UGtwRTRJZ3plc0hGQ0ZFQWoxWldLCnpQdFRBQlkwZlJrUzBNc3UwMHYxOXloTTUrdFUwYkVCZWo2eWpzWHRoYzlwS01hcUNIZWlQTC9TSHRkaWsxNVMKQmU4Sml4dVJxZitUeGlYWWVuNTg2aDlzTFpEYzA3cGpkUGp2NVNYRnBYQjhIMlVxQ0tZY2p4R3RvQWpTV0pjWApMNHc3RHNEby80bVg1N0htR09iamlCN1ZyOGhVWEJDdFh2V0dmQXlmcEFZNS9vOXowdm4zREcxaDc1NVVwdDluCkF2MFZrbm9qcmJVYjM1ZlJuU1lYTVltS01LSnpNRlMrdmFvRlpwV0ZjTG10cFRWSWNzc0JGUEYyZEo3V1c0WHMKK0d2Vkl2eFl3S2wyZzFPTE1TTXRZa09vekdlblBXTzdIdU0yMUVKVGIvbHNEZ25GaTkrYWRGZHBLY3R2cm0zdgpmbW1HeG5pRmhLU05GU0xtNms5YStHL2pjK3NVQVBhb2FZNEQ3NHVGajh0WGp0eThFUHdRRGxVUGRVZld3SE9PClF3bVgyMys1REh4V0VoQy91Tm8yNHNNY2ZkQzFGZUpBV281bUNuVU5vUVVmMStNRDVhMzNJdDhhMmlrNUkxUWoKeSs1WGpRaG0xd3RBMWhWTWE4aUxBR0toT09lcFRuK1VBZHpyS0hvNjVtYzNKbGgvSFJDUXJabnVxWkErK0F2WgpjeWU0dWZGWC8xdmRQSTdLb2Q0MEdDM2dlQnhweFFNYnp1OFNUcGpOcElJRkJvRVc5dFRhemUzeHZXWnV6dDc0CnFjZS8xWURuUHBLeW5lM0xGMk94VWoyYWVYUW5YQkpYcGhTZTBVTGJMcWJtUll4bjJKWkl1d09RNHV5dm94NjUKdG9TWGNac054dUs4QTErZXNXR3JSN3pVc0djdU9QQTFERE9Ja2JjcGtmRUxMNjk4RTJRckdqTU9JWnhrcWdxZQoySE5VNktWRmV2NzdZeEJDbm1VcVdXZEhYMjcyU2NPMUYzdWpUdFVnRVBNWGN0aEdBckYzTWxEaUw1Q0k0RkhqCnhHc3pVemxzalRQTmpiY2MzdUE2MjVZS3VVZEI2c1h1Rk5NUHk5UDgwTzBpRWJGTXl3MWxmN2VpdFhvaUUxWVoKc3NhMDVxTUx4M3pPUXZTLzFDdFpqaFp4cVJMRW5pQ3NWa2J..."Passphrase:
passphrase = "pwd1234"Unit Test Example
Section titled “Unit Test Example”defmodule Apify.SecretInputTest do use ExUnit.Case
test "decrypt encrypted string value" do # You would need to generate test data using the JS SDK # or use real encrypted values from Apify platform
input = %{ "username" => "john_doe", "password" => "ENCRYPTED_VALUE:abc123:def456" }
private_key = System.get_env("TEST_PRIVATE_KEY") passphrase = System.get_env("TEST_PASSPHRASE")
result = Apify.SecretInput.decrypt_secrets(input, private_key, passphrase)
assert result["username"] == "john_doe" assert result["password"] == "decrypted_password_here" endendReferences
Section titled “References”Source Code References
Section titled “Source Code References”-
JavaScript SDK - Actor Input Decryption
- File:
src/actor.ts:1347-1378 - Function:
Actor.getInput() - Shows how to load private key and decrypt input
- File:
-
Input Secrets Package
- File:
node_modules/@apify/input_secrets/esm/index.mjs:111-134 - Function:
decryptInputSecrets() - Shows regex pattern and decryption logic
- File:
-
Crypto Utilities
- File:
node_modules/@apify/utilities/esm/index.mjs:1756-1790 - Functions:
publicEncrypt()andprivateDecrypt() - Shows exact encryption/decryption implementation
- File:
-
Configuration
- File:
src/configuration.ts:164-166 - Shows environment variable names
- File:
External Documentation
Section titled “External Documentation”Implementation Checklist
Section titled “Implementation Checklist”- Implement
Apify.Configmodule to read environment variables - Implement
Apify.Crypto.RSAmodule for RSA decryption- Decode PEM-format private key
- Handle passphrase-encrypted keys
- RSA-2048 decryption with OAEP padding
- Implement
Apify.Crypto.AESmodule for AES-GCM decryption- Split password buffer into key and IV
- Split encrypted value into ciphertext and auth tag
- AES-256-GCM decryption
- Implement
Apify.SecretInputmodule- Regex pattern matching for encrypted values
- Handle both
ENCRYPTED_VALUEandENCRYPTED_JSONprefixes - Recursively decrypt all fields in input object
- Implement
Apify.Inputmodule- Read raw input from key-value store
- Check for decryption keys
- Decrypt if keys available
- Add error handling and logging
- Write unit tests with test data
- Test with real Apify Actor runs
Common Pitfalls
Section titled “Common Pitfalls”- RSA Padding: Ensure you use OAEP padding, not PKCS#1 v1.5
- Auth Tag Order: The auth tag is appended at the END of the encrypted value
- Base64 Encoding: Use standard Base64, not URL-safe variants
- Password Buffer Length: Must be exactly 48 bytes (32 + 16)
- JSON Parsing: Only parse if prefix is
ENCRYPTED_JSON - Private Key Format: The env var contains base64-encoded PEM, decode twice
Need Help?
Section titled “Need Help?”If you encounter issues:
- Check that environment variables are set correctly
- Verify the private key passphrase is correct
- Ensure the encrypted value format matches the regex
- Check RSA padding settings match Node.js crypto behavior
- Verify AES-GCM auth tag is being handled correctly
- Compare with JavaScript SDK test cases in
test/apify/actor.test.ts:1503-1534