Make WordPress Core

Opened 12 days ago

Last modified 42 minutes ago

#66187 accepted enhancement

Secrets API: encrypted, versioned storage for credentials

Reported by: ericmann Owned by: whyisjake
Priority: normal Milestone: 7.2
Component: Security Version:
Severity: normal Keywords: has-patch has-unit-tests needs-dev-note needs-docs
Cc: Focuses:

Description

The ​Secrets API proposal is for a core API that stores credentials such as API keys and tokens encrypted at rest, keeps a previous version for rotation, and lets hosts replace the storage through a secrets.php drop-in. This ticket covers the 7.2 part: the API itself. The admin screen is deferred to 7.3, as the proposal says. The WP-CLI commands stay in the feature plugin for now, since WP-CLI commands ship as WP-CLI packages rather than in core.

The patch is the src/ directory of the ​feature plugin, copied without changes, plus the wiring the plugin's bootstrap does today. The plugin has been public since 4 September and is at v0.2.0. The documentation, including every place the code departs from the proposal and why, is at ​secretsapi.dev.

This is also the storage #64789 asks for. The Connectors screen from #64730 keeps AI provider API keys as plaintext options, which are visible on options.php. Stored through this API, they are encrypted under a key that is not in the database, so a database dump or backup no longer exposes them. Moving Connectors onto wp_set_secret() and wp_get_secret() can follow once this lands.

The patch

  • The API: 14 new files in src/wp-includes/.
    • the public functions
    • WP_Secret and WP_Secret_Version
    • the provider, store and keyring interfaces, with their default and fail-closed implementations
    • the cipher and the key manager

src/wp-admin/includes/secrets-site-health.php adds three Site Health tests and a debug-information section.

  • Loading: wp-settings.php loads the API, then loads wp-content/secrets.php through a new wp_load_secrets_dropin() in load.php, just before must-use plugins.
  • Failing closed: if the drop-in throws, or sets a provider, store or keyring that does not implement its interface, every Secrets API call returns WP_Error for the rest of the request. Nothing falls back to the default provider: a broken drop-in has to read as unreachable, never as "this secret does not exist". See ​ADR 0007.
  • Capabilities: populate_roles_720() grants manage_secrets to administrators on install, and upgrade_720() grants it on upgrade. manage_network_secrets joins the other network capabilities in map_meta_cap(), so only super admins hold it.
  • Tests: the plugin's tests for everything in src/, 371 of them, in tests/phpunit/tests/secrets/. They come with provider and keyring conformance suites for hosts that write drop-ins.

Changes and additions beyond the proposal

The proposal names wp_set_secret(), wp_get_secret(), wp_delete_secret(), wp_import_option_as_secret(), WP_Secret, and WP_Secret_Version. Everything below differs from or goes beyond that, and is listed so each item can be reviewed, and rejected, on its own. The reasoning for each is on the linked page.

  • WP_Secret::reveal() returns string|WP_Error, not string: ​retrieval
  • wp_retire_secret_version() and wp_list_secrets(): ​rotation, ​retrieval
  • Network scope, which the proposal describes without naming functions: wp_set_network_secret(), wp_get_network_secret(), wp_delete_network_secret(), wp_retire_network_secret_version(), wp_list_network_secrets(). See ​network.
  • wp_secrets_validate_name(), wp_secrets_memzero(), wp_using_secrets_dropin(), wp_secrets_provider_is_writable(), wp_secrets_provider_label()
  • The wp_secret_changed action, for auditing, plus WP_Secret::get_name() and WP_Secret::withheld()
  • The WP_Secrets_Provider, WP_Secrets_Store, and WP_Secrets_Keyring interfaces, and their default and fail-closed implementations. The provider replaced the proposal's two-part drop-in contract after hosts on the proposal thread showed it could not describe how they already deploy. See ​providers and keyrings and ​ADR 0001.
  • Constants: ten WP_SECRETS_ERROR_* codes, WP_SECRETS_RECORD_VERSION, WP_SECRETS_MAX_NAME_LENGTH, WP_SECRETS_CAP_MANAGE, and WP_SECRETS_CAP_MANAGE_NETWORK

The ​scope page has the complete list.

Testing

  • In this branch:
    • npm run test:php -- --group secrets passes on single site and multisite (371 tests each).
    • tests/phpunit/tests/user/capabilities.php passes with both capabilities added to its inventory.
    • The full single-site suite shows no new failures.
  • The plugin's CI: PHP 7.4, 8.0 and 8.3, each against WordPress latest and trunk, plus a multisite job and a WP-CLI smoke test on PHP 7.4 and 8.3.
  • Drop-in examples: the ones in the plugin's examples/ directory have been run against live AWS KMS and AWS Secrets Manager, against HashiCorp Vault 2.1.1 (including a sealed server), and against OpenBao 2.7.0.

Notes for committers

  • $wp_db_version is set to a placeholder, 61900, which upgrade_720() checks. It needs the real revision number on commit.
  • wp-tests-config-sample.php ships placeholder salts, and the default keyring deliberately refuses to derive a key from them. When the salts are placeholders, the test bootstrap therefore defines a fixed WP_SECRETS_KEY in the main PHPUnit process. The four tests that exercise the salt-based fallback are skipped in that case.
  • Every new symbol is marked @since 7.2.0.

Change History (13)

This ticket was mentioned in ​PR #13759 on ​WordPress/wordpress-develop by ​@ericmann.


12 days ago
#1

Adds the Secrets API for 7.2: encrypted, versioned storage for credentials, with a secrets.php drop-in for hosts. The ticket has the full description and the list of additions beyond the ​proposal. The documentation is at ​secretsapi.dev.

Three commits, meant to be reviewed separately:

  1. Add the API from the feature plugin. This is the ​feature plugin's src/ at v0.2.0, copied unchanged: 14 files in src/wp-includes/ and the Site Health tests.
  2. Load the API, the drop-in, and the capabilities.
    • wp-settings.php loads the API, then loads wp-content/secrets.php through a new wp_load_secrets_dropin() in load.php, just before must-use plugins.
    • A broken drop-in fails closed with WP_Error and never falls back to the default provider.
    • class-wp-site-health.php loads the secrets Site Health tests.
    • populate_roles_720() and upgrade_720() grant manage_secrets to administrators.
    • manage_network_secrets goes with the network capabilities in map_meta_cap().
    • $wp_db_version is a placeholder, 61900, which needs the real revision on commit.
  3. Tests. 371 tests in tests/phpunit/tests/secrets/, with helpers in tests/phpunit/includes/secrets/, plus the capability inventory.
    • The test bootstrap defines a fixed WP_SECRETS_KEY when wp-tests-config.php has the placeholder salts, which the default keyring refuses.
    • Four tests that exercise the salt-based fallback skip in that case.

Testing:

  • npm run test:php -- --group secrets passes on single site and multisite.
  • tests/phpunit/tests/user/capabilities.php passes.
  • The full single-site suite shows no new failures.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5, Claude Sonnet 5, Claude Fable 5.1
Used for: Most of the code and tests, in the feature plugin and in this port: the API, the core wiring, the test port, and review passes. I directed the design, and the decisions are recorded as ADRs at secretsapi.dev.

#2 @whyisjake
12 days ago

  • Owner set to whyisjake
  • Status new → accepted

This ticket was mentioned in ​Slack in #core by ericmann. ​View the logs.


8 days ago

This ticket was mentioned in ​Slack in #core by amykamala. ​View the logs.


7 days ago

#5 follow-up: @whyisjake
7 days ago

@ericmann I wonder if we should put everything in the includes folder into a folder. Just looking at the abilities-api/ai/etc. as an example.

#6 in reply to: ↑ 5 @ericmann
43 hours ago

Replying to whyisjake:

@ericmann I wonder if we should put everything in the includes folder into a folder. Just looking at the abilities-api/ai/etc. as an example.

@whyisjake Agreed, and done. Everything now lives in src/wp-includes/secrets/, following the layout abilities-api/ and ai-client/ use. The PR has it as a separate commit, so it reads as 13 renames plus the require paths in wp-settings.php. Nothing else changes.

Only two things stayed where they were:

  • src/wp-includes/secrets.php, the functions file, stays at the top level next to the folder, the way abilities-api.php and ai-client.php do.
  • src/wp-admin/includes/secrets-site-health.php stays in wp-admin/includes/.

If you would rather have secrets.php inside the folder too, that is a small change and I am happy to make it. The feature plugin has the same layout as of today, so its src/ and the patch are still identical.

#7 @whyisjake
42 hours ago

  • Keywords needs-dev-note needs-docs added

That sounds good to me. Let's get merged this week.

This ticket was mentioned in ​Slack in #core by whyisjake. ​View the logs.


5 hours ago

​@westonruter commented on ​PR #13759:


4 hours ago
#9

I ran PHPStan rule level 10 on the changes in this PR and there were quite a few opportunities for improvement. For the changes in src:

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/schema.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  977    Function populate_roles_720() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/secrets-site-health.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  23     Function wp_secrets_site_health_tests() has parameter $tests with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  23     Function wp_secrets_site_health_tests() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  24     Cannot access offset 'secrets_api_key_source' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  29     Cannot access offset 'secrets_api_undecryptable' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  34     Cannot access offset 'secrets_api_needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  56     Function wp_secrets_site_health_result() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  82     Function wp_secrets_site_health_test_key_source() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  139    Function wp_secrets_site_health_test_undecryptable() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  160    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  160    Parameter #1 $text of function esc_html expects float|int|string, mixed given.
         🪪  argument.type
  164    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  164    Parameter #1 $text of function esc_html expects float|int|string, mixed given.
         🪪  argument.type
  191    Function wp_secrets_site_health_find_undecryptable() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  201    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  201    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  201    Parameter #1 $name of function wp_get_network_secret expects string, mixed given.
         🪪  argument.type
  201    Parameter #1 $name of function wp_get_secret expects string, mixed given.
         🪪  argument.type
  205    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  206    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  221    Function wp_secrets_site_health_test_needs_rotation() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  278    Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  298    Function wp_secrets_site_health_debug_info() has parameter $info with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  298    Function wp_secrets_site_health_debug_info() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/upgrade.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  2517   Function upgrade_720() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/load.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  848    Function wp_load_secrets_dropin() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  169    Function wp_secrets_memzero() has no return type specified.
         🪪  missingType.return
  325    Function _wp_secrets_get_store() should return WP_Secrets_Store but returns mixed.
         🪪  return.type
  361    Function _wp_secrets_get_key_manager() should return WP_Secrets_Key_Manager but returns mixed.
         🪪  return.type
  549    Function wp_list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  622    Function wp_list_network_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  655    Function _wp_secrets_get_provider() should return WP_Secrets_Provider but returns mixed.
         🪪  return.type
  809    Function _wp_secrets_list() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  239    Method WP_Secret::__debugInfo() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  308    Method WP_Secret::__unserialize() has parameter $data with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-broken-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  51     Method WP_Secrets_Broken_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-cipher.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  69     Method WP_Secrets_Cipher::encrypt_value() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  161    Parameter #1 $ciphertext of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  163    Parameter #3 $nonce of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  175    Parameter #1 $ciphertext of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  177    Parameter #3 $nonce of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, mixed given.
         🪪  argument.type
  178    Parameter #4 $key of function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt expects string, string|true given.
         🪪  argument.type
  181    Parameter #1 $value of function wp_secrets_memzero expects string, string|true given.
         🪪  argument.type
  190    Method WP_Secrets_Cipher::decrypt_value() should return string|WP_Error but returns string|true.
         🪪  return.type
  356    Method WP_Secrets_Cipher::decode_record_fields() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-config-key-provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  171    Method WP_Secrets_Config_Key_Provider::unwrap() should return string|WP_Error but returns string|true.
         🪪  return.type
  183    Parameter #1 $value of method WP_Secrets_Config_Key_Provider::is_canonical_base64_32() expects string, mixed given.
         🪪  argument.type
  216    Method WP_Secrets_Config_Key_Provider::get_site_key() should return string|WP_Error but returns string|false.
         🪪  return.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-key-manager.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  217    Method WP_Secrets_Key_Manager::get_root_key() should return string|WP_Error but returns string|null.
         🪪  return.type
  264    Parameter #1 $key_material of method WP_Secrets_Keyring::wrap() expects string, string|null given.
         🪪  argument.type
  267    Parameter #1 $value of function wp_secrets_memzero expects string, string|null given.
         🪪  argument.type
  281    Parameter #1 $value of function wp_secrets_memzero expects string, string|null given.
         🪪  argument.type
  292    Parameter #1 $value of function wp_secrets_memzero expects string, string|null given.
         🪪  argument.type
  315    Method WP_Secrets_Key_Manager::get_wrapped_root_key() should return string|false but returns mixed.
         🪪  return.type
  330    Method WP_Secrets_Key_Manager::get_wrapped_root_key() should return string|false but returns mixed.
         🪪  return.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-libsodium-provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  174    Method WP_Secrets_Libsodium_Provider::read_prior_record() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  198    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  230    Method WP_Secrets_Libsodium_Provider::demote_slot() has parameter $current_slot with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  230    Method WP_Secrets_Libsodium_Provider::demote_slot() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  509    do_action() call for hook "wp_secret_changed" references documentation in "src/wp-includes/secrets.php", but no such file exists in the tree being
         analyzed.
         🪪  wordpress.hookDocReferenceFileMissing
  565    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  578    do_action() call for hook "wp_secret_changed" references documentation in "src/wp-includes/secrets.php", but no such file exists in the tree being
         analyzed.
         🪪  wordpress.hookDocReferenceFileMissing
  620    Method WP_Secrets_Libsodium_Provider::list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  633    Parameter #1 $haystack of function strpos expects string, mixed given.
         🪪  argument.type
  637    Parameter #1 $name of method WP_Secrets_Store::get() expects string, mixed given.
         🪪  argument.type
  643    Cannot access offset 'created' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  645    Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/class-wp-secrets-option-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  52     Method WP_Secrets_Option_Store::get() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  81     Method WP_Secrets_Option_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  160    Method WP_Secrets_Option_Store::list_names() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  164    Binary operation "." between mixed and '%' results in an error.
         🪪  binaryOp.invalid
  164    Cannot call method esc_like() on mixed.
         🪪  method.nonObject
  167    Cannot call method get_col() on mixed.
         🪪  method.nonObject
  168    Cannot call method prepare() on mixed.
         🪪  method.nonObject
  169    Cannot access property $sitemeta on mixed.
         🪪  property.nonObject
  169    Part $wpdb->sitemeta (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  175    Cannot call method get_col() on mixed.
         🪪  method.nonObject
  176    Cannot call method prepare() on mixed.
         🪪  method.nonObject
  177    Cannot access property $options on mixed.
         🪪  property.nonObject
  177    Part $wpdb->options (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  193    Parameter #1 $string of function substr expects string, mixed given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/interface-wp-secrets-provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  173    Method WP_Secrets_Provider::list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-includes/secrets/interface-wp-secrets-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  38     Method WP_Secrets_Store::get() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  52     Method WP_Secrets_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  76     Method WP_Secrets_Store::list_names() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 [ERROR] Found 85 errors on changed lines (base: trunk) 

<details><summary>Errors in <code>tests</code></summary>

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/bootstrap.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  42     Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-mock-keyring.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  12     Property Mock_Keyring::$fail_wrap has no type specified.
         🪪  missingType.property
  13     Property Mock_Keyring::$fail_unwrap has no type specified.
         🪪  missingType.property
  14     Property Mock_Keyring::$wrap_calls has no type specified.
         🪪  missingType.property
  15     Property Mock_Keyring::$unwrap_calls has no type specified.
         🪪  missingType.property
  18     Cannot use ++ on mixed.
         🪪  preInc.type
  31     Cannot use ++ on mixed.
         🪪  preInc.type
  88     Method Mock_Keyring::wrap_call_count() should return int but returns mixed.
         🪪  return.type
  95     Method Mock_Keyring::unwrap_call_count() should return int but returns mixed.
         🪪  return.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-mock-store.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  10     Property Mock_Store::$records has no type specified.
         🪪  missingType.property
  11     Property Mock_Store::$fail has no type specified.
         🪪  missingType.property
  17     Property Mock_Store::$received_records has no type specified.
         🪪  missingType.property
  19     Method Mock_Store::get() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  20     Cannot access offset 'get' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  26     Method Mock_Store::get() should return array|WP_Error|null but returns mixed.
         🪪  return.type
  26     Parameter #1 $key of function array_key_exists expects int|string, mixed given.
         🪪  argument.type
  26     Parameter #2 $search of function array_key_exists expects array, mixed given.
         🪪  argument.type
  26     Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  29     Method Mock_Store::set() has parameter $record with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  30     Cannot access an offset on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  32     Cannot access offset 'set' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  36     Cannot access offset mixed on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  42     Cannot access offset 'delete' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  46     Cannot access offset mixed on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  51     Method Mock_Store::list_names() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  52     Cannot access offset 'list' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  59     Parameter #1 $input of function array_keys expects array, mixed given.
         🪪  argument.type
  60     Parameter #1 $haystack of function strpos expects string, int|string given.
         🪪  argument.type
  61     Parameter #1 $string of function substr expects string, int|string given.
         🪪  argument.type
  75     Cannot access offset string on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  87     Method Mock_Store::get_received_records() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  88     Method Mock_Store::get_received_records() should return array but returns mixed.
         🪪  return.type
  91     Method Mock_Store::key() has no return type specified.
         🪪  missingType.return
  91     Method Mock_Store::key() has parameter $name with no type specified.
         🪪  missingType.parameter
  91     Method Mock_Store::key() has parameter $network with no type specified.
         🪪  missingType.parameter
  92     Binary operation "." between 'network:'|'site:' and mixed results in an error.
         🪪  binaryOp.invalid
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-wp-secrets-keyring-conformance.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  29     Method WP_Secrets_Keyring_Conformance::test_wrap_returns_a_non_empty_string_that_unwraps_to_the_same_bytes() has no return type specified.
         🪪  missingType.return
  49     Method WP_Secrets_Keyring_Conformance::test_two_wraps_of_the_same_bytes_return_different_strings() has no return type specified.
         🪪  missingType.return
  65     Method WP_Secrets_Keyring_Conformance::test_unwrap_of_garbage_is_a_wp_error() has no return type specified.
         🪪  missingType.return
  77     Method WP_Secrets_Keyring_Conformance::test_unwrap_of_a_truncated_value_is_a_wp_error() has no return type specified.
         🪪  missingType.return
  81     Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  81     Parameter #1 $string of function substr expects string, string|WP_Error given.
         🪪  argument.type
  91     Method WP_Secrets_Keyring_Conformance::test_unwrap_of_a_value_with_one_flipped_byte_is_a_wp_error() has no return type specified.
         🪪  missingType.return
  95     Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  97     Cannot access offset int on string|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  97     Cannot access offset int on string|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  97     Parameter #1 $character of function ord expects string, mixed given.
         🪪  argument.type
  99     Parameter #1 $wrapped of method WP_Secrets_Keyring::unwrap() expects string, (ArrayAccess&WP_Error)|string given.
         🪪  argument.type
  106    Method WP_Secrets_Keyring_Conformance::test_get_key_source_returns_a_non_empty_string() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/includes/secrets/class-wp-secrets-provider-conformance.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  53     Method WP_Secrets_Provider_Conformance::require_writable() has no return type specified.
         🪪  missingType.return
  61     Method WP_Secrets_Provider_Conformance::test_reports_a_non_empty_label() has no return type specified.
         🪪  missingType.return
  68     Method WP_Secrets_Provider_Conformance::test_reports_a_known_protection_boundary() has no return type specified.
         🪪  missingType.return
  76     Method WP_Secrets_Provider_Conformance::test_is_writable_returns_a_bool() has no return type specified.
         🪪  missingType.return
  84     Method WP_Secrets_Provider_Conformance::test_a_read_only_provider_actually_refuses_writes() has no return type specified.
         🪪  missingType.return
  109    Method WP_Secrets_Provider_Conformance::test_a_name_that_was_never_set_is_null() has no return type specified.
         🪪  missingType.return
  120    Method WP_Secrets_Provider_Conformance::test_previous_on_a_provider_or_secret_without_history_is_null_not_error() has no return type specified.
         🪪  missingType.return
  133    Method WP_Secrets_Provider_Conformance::test_deleting_something_absent_is_success() has no return type specified.
         🪪  missingType.return
  143    Method WP_Secrets_Provider_Conformance::test_retiring_with_no_previous_version_is_success() has no return type specified.
         🪪  missingType.return
  155    Method WP_Secrets_Provider_Conformance::test_a_stored_value_comes_back_intact() has no return type specified.
         🪪  missingType.return
  174    Method WP_Secrets_Provider_Conformance::test_a_fingerprint_is_stable_for_the_same_value() has no return type specified.
         🪪  missingType.return
  181    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  182    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  189    Method WP_Secrets_Provider_Conformance::test_a_deleted_secret_reads_as_absent() has no return type specified.
         🪪  missingType.return
  202    Method WP_Secrets_Provider_Conformance::test_listing_returns_an_array_or_an_error_and_never_a_value() has no return type specified.
         🪪  missingType.return
  214    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, string|false given.
         🪪  argument.type
  219    Method WP_Secrets_Provider_Conformance::test_listing_includes_a_stored_secret() has no return type specified.
         🪪  missingType.return
  226    Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
  236    Method WP_Secrets_Provider_Conformance::test_listing_respects_a_name_prefix() has no return type specified.
         🪪  missingType.return
  243    Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/brokenDropinFallbacks.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  12     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Store' and WP_Secrets_Broken_Store will always evaluate to true.
         🪪  method.alreadyNarrowedType
  42     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Keyring' and WP_Secrets_Broken_Keyring will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/capabilities.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  30     Call to method PHPUnit\Framework\Assert::assertSame() with 'manage_secrets' and 'manage_secrets' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  31     Call to method PHPUnit\Framework\Assert::assertSame() with 'manage_network…' and 'manage_network…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  35     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  40     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  45     Cannot call method remove_cap() on WP_Role|null.
         🪪  method.nonObject
  49     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  64     Cannot call method remove_cap() on WP_Role|null.
         🪪  method.nonObject
  70     Cannot call method has_cap() on WP_Role|null.
         🪪  method.nonObject
  73     Method Tests_Secrets_Capabilities::data_upgrade_720_gate() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/errorCodes.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  11     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_decryption…' and 'secret_decryption…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  12     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_key…' and 'secret_key…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  13     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_store…' and 'secret_store…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  14     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_invalid_name' and 'secret_invalid_name' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  15     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_invalid_value' and 'secret_invalid_value' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  16     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_crypto…' and 'secret_crypto…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
  17     Call to method PHPUnit\Framework\Assert::assertSame() with 'secret_record…' and 'secret_record…' will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/extensionPoints.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  38     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  49     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  61     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  110    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/networkScope.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  28     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  29     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  45     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  68     Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/provider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  22     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::set() never returns true so it can be removed from the return type.
         🪪  return.unusedType
  29     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::delete() never returns true so it can be removed from the return type.
         🪪  return.unusedType
  37     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::list_secrets() never returns WP_Error so it can be removed from the return type.
         🪪  return.unusedType
  37     Method WP_Secrets_Provider@anonymous/tests/secrets/provider.php:17::list_secrets() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  150    Call to method PHPUnit\Framework\Assert::assertNotInstanceOf() with 'WP_Secrets…' and WP_Secrets_Broken_Provider will always evaluate to true.
         🪪  method.alreadyNarrowedType
  214    Call to method PHPUnit\Framework\Assert::assertNotNull() with WP_Error will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/readonlyProviderConformance.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  20     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::set() never returns true so it can be removed from the return type.
         🪪  return.unusedType
  27     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::delete() never returns true so it can be removed from the return
         type.
         🪪  return.unusedType
  31     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::retire_previous() never returns true so it can be removed from the
         return type.
         🪪  return.unusedType
  35     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::list_secrets() never returns WP_Error so it can be removed from the
         return type.
         🪪  return.unusedType
  35     Method WP_Secrets_Provider@anonymous/tests/secrets/readonlyProviderConformance.php:15::list_secrets() return type has no value type specified in iterable
         type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/siteHealth.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  13     Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  17     Argument of an invalid type array|ArrayAccess supplied for foreach, only iterables are supported.
         🪪  foreach.nonIterable
  18     Cannot access offset 'test' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  18     Cannot access offset 'test' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  18     Parameter #1 $function_name of function function_exists expects string, mixed given.
         🪪  argument.type
  18     Part $test['test'] (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  25     Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  29     Result of || is always true.
         🪪  booleanOr.alwaysTrue
  29     Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  29     Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  33     Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  49     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  92     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  95     Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  95     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  101    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  102    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  109    Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  109    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  114    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, mixed given.
         🪪  argument.type
  132    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringContainsString() expects string, mixed given.
         🪪  argument.type
  141    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  143    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  150    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  151    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  152    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  153    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  159    Cannot access offset 'dropin_active' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  159    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  159    Cannot access offset 'value' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  169    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, string|false given.
         🪪  argument.type
  175    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  175    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayNotHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  191    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  191    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayNotHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  196    Cannot access offset 'fields' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  196    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/threeStateContract.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  103    Result of || is always true.
         🪪  booleanOr.alwaysTrue
  103    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  103    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  107    Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  152    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  152    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  169    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  172    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  172    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  175    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/versionSlots.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  45     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  49     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  54     Cannot access offset 'created' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  54     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  58     Cannot access offset 'created' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  58     Cannot access offset 'previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  69     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  69     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  74     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  74     Cannot access offset 'previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  86     Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  86     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  90     Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  90     Cannot access offset 'previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  94     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  113    Cannot access offset 'ct' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  113    Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  117    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  129    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  151    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  166    Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpDeleteSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  29     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  43     Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpImportOptionAsSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  13     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  30     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  30     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  38     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  38     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  81     Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
  98     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  99     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpListSecrets.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  18     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, array|WP_Error given.
         🪪  argument.type
  21     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  21     Parameter #1 $input of function array_keys expects array, mixed given.
         🪪  argument.type
  23     Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  23     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  35     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, string|false given.
         🪪  argument.type
  41     Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  44     Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  44     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  50     Cannot access offset 'has_previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  50     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  54     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  61     Cannot access offset 'current' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  61     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  64     Cannot access offset 'needs_rotation' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  64     Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  71     Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
  80     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, array|WP_Error given.
         🪪  argument.type
  93     Parameter #1 $namespace of function wp_list_secrets expects string, array<int, string> given.
         🪪  argument.type
  108    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertCount() expects Countable|iterable, array|WP_Error given.
         🪪  argument.type
  109    Cannot access offset 'name' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  109    Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  110    Cannot access offset 'fingerprint' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  110    Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  111    Cannot access offset 'has_previous' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  111    Cannot access offset 0 on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  118    Parameter #1 $input_list of function wp_list_pluck expects array, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  21     Method Tests_Secrets_WPSecret::make_secret() has no return type specified.
         🪪  missingType.return
  38     Cannot call method reveal() on mixed.
         🪪  method.nonObject
  44     Cannot call method fingerprint() on mixed.
         🪪  method.nonObject
  50     Cannot call method get_name() on mixed.
         🪪  method.nonObject
  56     Parameter #2 $value of class WP_Secret constructor expects string, int given.
         🪪  argument.type
  76     Method Tests_Secrets_WPSecret::test_masking_surfaces_never_contain_the_plaintext() has parameter $callback with no type specified.
         🪪  missingType.parameter
  79     Trying to invoke mixed but it's not a callable.
         🪪  callable.nonCallable
  81     Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertStringNotContainsString() expects string, mixed given.
         🪪  argument.type
  84     Method Tests_Secrets_WPSecret::data_masking_surfaces() has no return type specified.
         🪪  missingType.return
  88     Cannot cast mixed to string.
         🪪  cast.string
  93     Part $secret (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  122    Parameter #1 $message of function error_log expects string, mixed given.
         🪪  argument.type
  135    Cannot cast mixed to string.
         🪪  cast.string
  142    Parameter #1 $json of function json_decode expects string, string|false given.
         🪪  argument.type
  168    Cannot clone non-object variable $secret of type mixed.
         🪪  clone.nonObject
  168    Expression "clone $secret" on a separate line does not do anything.
         🪪  expr.resultUnused
  178    Method Tests_Secrets_WPSecret::test_serialization_magic_methods_throw_directly() has parameter $args with no type specified.
         🪪  missingType.parameter
  178    Method Tests_Secrets_WPSecret::test_serialization_magic_methods_throw_directly() has parameter $method with no type specified.
         🪪  missingType.parameter
  180    Parameter #1 $objectOrMethod of class ReflectionMethod constructor expects object|string, mixed given.
         🪪  argument.type
  180    Parameter #2 $method of class ReflectionMethod constructor expects string, mixed given.
         🪪  argument.type
  187    Parameter #1 $object of method ReflectionMethod::invokeArgs() expects object|null, mixed given.
         🪪  argument.type
  187    Parameter #2 $args of method ReflectionMethod::invokeArgs() expects array, mixed given.
         🪪  argument.type
  190    Method Tests_Secrets_WPSecret::data_refusing_magic_methods() has no return type specified.
         🪪  missingType.return
  201    Parameter #1 $obj of function spl_object_id expects object, mixed given.
         🪪  argument.type
  208    Parameter #2 $array of method PHPUnit\Framework\Assert::assertArrayHasKey() expects array|ArrayAccess, mixed given.
         🪪  argument.type
  318    Parameter #3 $reason of static method WP_Secret::withheld() expects WP_Error, string given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsCipher.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has no return type specified.
         🪪  missingType.return
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has parameter $args with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has parameter $instance with no type specified.
         🪪  missingType.parameter
  11     Method Tests_Secrets_WPSecretsCipher::invoke_private() has parameter $method with no type specified.
         🪪  missingType.parameter
  12     Parameter #1 $objectOrMethod of class ReflectionMethod constructor expects object|string, mixed given.
         🪪  argument.type
  12     Parameter #2 $method of class ReflectionMethod constructor expects string, mixed given.
         🪪  argument.type
  17     Parameter #1 $object of method ReflectionMethod::invokeArgs() expects object|null, mixed given.
         🪪  argument.type
  20     Method Tests_Secrets_WPSecretsCipher::master_key() has no return type specified.
         🪪  missingType.return
  20     Method Tests_Secrets_WPSecretsCipher::master_key() has parameter $byte with no type specified.
         🪪  missingType.parameter
  21     Parameter #1 $ascii of function chr expects int, mixed given.
         🪪  argument.type
  26     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  30     Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  37     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  39     Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  46     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  48     Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  56     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  56     Parameter #6 $plaintext of method WP_Secrets_Cipher::encrypt_value() expects string, array given.
         🪪  argument.type
  65     Method Tests_Secrets_WPSecretsCipher::test_encrypt_rejects_a_bad_master_key() has parameter $bad_key with no type specified.
         🪪  missingType.parameter
  68     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  74     Method Tests_Secrets_WPSecretsCipher::data_bad_master_keys() has no return type specified.
         🪪  missingType.return
  86     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  97     Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  108    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  119    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $name with no type specified.
         🪪  missingType.parameter
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $scope with no type specified.
         🪪  missingType.parameter
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $site_id with no type specified.
         🪪  missingType.parameter
  131    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_context_does_not_match() has parameter $slot with no type specified.
         🪪  missingType.parameter
  133    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #2 $scope of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #3 $site_id of method WP_Secrets_Cipher::decrypt_value() expects int, mixed given.
         🪪  argument.type
  135    Parameter #4 $name of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  135    Parameter #5 $slot of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  141    Method Tests_Secrets_WPSecretsCipher::data_mismatched_context() has no return type specified.
         🪪  missingType.return
  152    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  154    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  163    Method Tests_Secrets_WPSecretsCipher::test_decrypt_fails_when_a_field_is_tampered() has parameter $field with no type specified.
         🪪  missingType.parameter
  165    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  167    Cannot access offset mixed on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  167    Parameter #1 $str of function base64_decode expects string, mixed given.
         🪪  argument.type
  167    Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  168    Parameter #1 $string of function strlen expects string, string|false given.
         🪪  argument.type
  169    Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  169    Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  170    Cannot access offset mixed on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  170    Possibly invalid array key type mixed.
         🪪  offsetAccess.invalidOffset
  172    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  177    Method Tests_Secrets_WPSecretsCipher::data_tamperable_fields() has no return type specified.
         🪪  missingType.return
  189    Method Tests_Secrets_WPSecretsCipher::test_decrypt_rejects_a_malformed_record() has parameter $record with no type specified.
         🪪  missingType.parameter
  192    Parameter #1 $master_key of method WP_Secrets_Cipher::decrypt_value() expects string, mixed given.
         🪪  argument.type
  198    Method Tests_Secrets_WPSecretsCipher::data_malformed_records() has no return type specified.
         🪪  missingType.return
  230    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  231    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  233    Cannot access offset 'dk' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  233    Cannot access offset 'dk' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  234    Cannot access offset 'ct' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  234    Cannot access offset 'ct' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  242    Parameter #1 $master_key of method WP_Secrets_Cipher::encrypt_value() expects string, mixed given.
         🪪  argument.type
  243    Cannot access offset 'nonce' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  244    Cannot access offset 'dk_nonce' on array|WP_Error.
         🪪  offsetAccess.nonOffsetAccessible
  247    Parameter #1 $array of function array_unique expects an array of values castable to string, list given.
         🪪  argument.type
  254    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  255    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  263    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  264    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  272    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  273    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  289    Parameter #1 $master_key of method WP_Secrets_Cipher::fingerprint() expects string, mixed given.
         🪪  argument.type
  289    Parameter #2 $plaintext of method WP_Secrets_Cipher::fingerprint() expects string, array given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsConfigKeyProvider.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has no return type specified.
         🪪  missingType.return
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has parameter $args with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         🪪  missingType.iterableValue
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has parameter $instance with no type specified.
         🪪  missingType.parameter
  16     Method Tests_Secrets_WPSecretsConfigKeyProvider::invoke_private() has parameter $method with no type specified.
         🪪  missingType.parameter
  17     Parameter #1 $objectOrMethod of class ReflectionMethod constructor expects object|string, mixed given.
         🪪  argument.type
  17     Parameter #2 $method of class ReflectionMethod constructor expects string, mixed given.
         🪪  argument.type
  22     Parameter #1 $object of method ReflectionMethod::invokeArgs() expects object|null, mixed given.
         🪪  argument.type
  26     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Keyring' and WP_Secrets_Config_Key_Provider will always evaluate to true.
         🪪  method.alreadyNarrowedType
  43     Parameter #1 $key_material of method WP_Secrets_Config_Key_Provider::wrap() expects string, int given.
         🪪  argument.type
  68     Parameter #1 $wrapped of method WP_Secrets_Config_Key_Provider::unwrap() expects string, array given.
         🪪  argument.type
  96     Parameter #1 $str of function base64_decode expects string, string|WP_Error given.
         🪪  argument.type
  98     Parameter #1 $string of function strlen expects string, string|false given.
         🪪  argument.type
  99     Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  99     Cannot access offset int<-1, max> on string|false.
         🪪  offsetAccess.nonOffsetAccessible
  108    Result of || is always true.
         🪪  booleanOr.alwaysTrue
  108    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  108    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  112    Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  120    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_is_canonical_base64_32() has parameter $expected with no type specified.
         🪪  missingType.parameter
  120    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_is_canonical_base64_32() has parameter $value with no type specified.
         🪪  missingType.parameter
  126    Method Tests_Secrets_WPSecretsConfigKeyProvider::data_canonical_base64_32() has no return type specified.
         🪪  missingType.return
  141    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_are_usable_salt_values() has parameter $expected with no type specified.
         🪪  missingType.parameter
  141    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_are_usable_salt_values() has parameter $key with no type specified.
         🪪  missingType.parameter
  141    Method Tests_Secrets_WPSecretsConfigKeyProvider::test_are_usable_salt_values() has parameter $salt with no type specified.
         🪪  missingType.parameter
  147    Method Tests_Secrets_WPSecretsConfigKeyProvider::data_usable_salt_values() has no return type specified.
         🪪  missingType.return
  188    Parameter #1 $data of function bin2hex expects string, mixed given.
         🪪  argument.type
  271    Result of || is always true.
         🪪  booleanOr.alwaysTrue
  271    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  271    Strict comparison using === between 'put your unique…' and 'put your unique…' will always evaluate to true.
         🪪  identical.alwaysTrue
  275    Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  312    Parameter #1 $wrapped of method WP_Secrets_Config_Key_Provider::unwrap() expects string, string|WP_Error given.
         🪪  argument.type
  313    Parameter #1 $wrapped of method WP_Secrets_Config_Key_Provider::unwrap() expects string, string|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsKeyManager.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  52     Parameter #4 $key of function sodium_crypto_kdf_derive_from_key expects string, string|WP_Error given.
         🪪  argument.type
  107    Method Tests_Secrets_WPSecretsKeyManager::test_get_master_key_reports_an_invalid_site_id_as_a_wp_error() has parameter $site_id with no type specified.
         🪪  missingType.parameter
  112    Parameter #2 $site_id of method WP_Secrets_Key_Manager::get_master_key() expects int|null, mixed given.
         🪪  argument.type
  118    Method Tests_Secrets_WPSecretsKeyManager::data_invalid_site_ids() has no return type specified.
         🪪  missingType.return
  196    Parameter #1 $known_string of function hash_equals expects string, string|WP_Error given.
         🪪  argument.type
  196    Parameter #2 $user_string of function hash_equals expects string, string|WP_Error given.
         🪪  argument.type
  221    Cannot call method reveal() on WP_Secret|null.
         🪪  method.nonObject
  271    Method WP_Secrets_Keyring@anonymous/tests/secrets/wpSecretsKeyManager.php:270::wrap() never returns WP_Error so it can be removed from the return type.
         🪪  return.unusedType
  274    Method WP_Secrets_Keyring@anonymous/tests/secrets/wpSecretsKeyManager.php:270::unwrap() never returns string so it can be removed from the return type.
         🪪  return.unusedType
  332    Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  333    Parameter #1 $string of function strlen expects string, string|WP_Error given.
         🪪  argument.type
  405    Parameter #1 $expected of method PHPUnit\Framework\Assert::assertSame() contains unresolvable type.
         🪪  argument.unresolvableType
  428    Parameter #1 $value of function wp_secrets_memzero expects string, string|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsMemzero.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  19     Parameter #1 $value of function wp_secrets_memzero expects string, int given.
         🪪  argument.type
  41     Call to method PHPUnit\Framework\Assert::assertSame() with 'shared plaintext' and 'shared plaintext' will always evaluate to true.
         🪪  method.alreadyNarrowedType
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsOptionStore.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  9      Method Tests_Secrets_WPSecretsOptionStore::sample_record() has no return type specified.
         🪪  missingType.return
  24     Call to method PHPUnit\Framework\Assert::assertInstanceOf() with 'WP_Secrets_Store' and WP_Secrets_Option_Store will always evaluate to true.
         🪪  method.alreadyNarrowedType
  37     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  45     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  52     Parameter #1 $arr1 of function array_merge expects array, mixed given.
         🪪  argument.type
  54     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  64     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, string given.
         🪪  argument.type
  83     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  99     Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  101    Cannot call method get_var() on mixed.
         🪪  method.nonObject
  102    Cannot call method prepare() on mixed.
         🪪  method.nonObject
  103    Cannot access property $options on mixed.
         🪪  property.nonObject
  103    Part $wpdb->options (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  113    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  114    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  118    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  126    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  128    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertNotContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  133    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  134    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  136    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  138    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
  153    Parameter #2 $record of method WP_Secrets_Option_Store::set() expects array, mixed given.
         🪪  argument.type
  157    Parameter #2 $haystack of method PHPUnit\Framework\Assert::assertContains() expects iterable, array|WP_Error given.
         🪪  argument.type
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSecretsValidateName.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  10     Method Tests_Secrets_WpSecretsValidateName::test_accepts_valid_names() has parameter $name with no type specified.
         🪪  missingType.parameter
  11     Parameter #1 $name of function wp_secrets_validate_name expects string, mixed given.
         🪪  argument.type
  14     Method Tests_Secrets_WpSecretsValidateName::data_valid_names() has no return type specified.
         🪪  missingType.return
  30     Method Tests_Secrets_WpSecretsValidateName::test_rejects_invalid_names() has parameter $name with no type specified.
         🪪  missingType.parameter
  31     Parameter #1 $name of function wp_secrets_validate_name expects string, mixed given.
         🪪  argument.type
  45     Method Tests_Secrets_WpSecretsValidateName::test_accepts_an_unnamespaced_name_but_reports_it() has parameter $name with no type specified.
         🪪  missingType.parameter
  48     Parameter #1 $name of function wp_secrets_validate_name expects string, mixed given.
         🪪  argument.type
  51     Method Tests_Secrets_WpSecretsValidateName::data_unnamespaced_names() has no return type specified.
         🪪  missingType.return
  72     Method Tests_Secrets_WpSecretsValidateName::data_invalid_names() has no return type specified.
         🪪  missingType.return
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/phpunit/tests/secrets/wpSetSecretAndWpGetSecret.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------
  30     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  31     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  71     Parameter #2 $value of function wp_set_secret expects string, array<int, string> given.
         🪪  argument.type
  79     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  90     Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  105    Cannot access property $options on mixed.
         🪪  property.nonObject
  105    Cannot call method get_results() on mixed.
         🪪  method.nonObject
  105    Part $wpdb->options (mixed) of encapsed string cannot be cast to string.
         🪪  encapsedStringPart.nonString
  120    Cannot call method reveal() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  154    Cannot call method fingerprint() on WP_Error|WP_Secret|null.
         🪪  method.nonObject
  168    Cannot use array destructuring on array<int<0, max>|string, mixed>|null.
         🪪  offsetAccess.nonArray
  207    Argument of an invalid type array<int<0, max>|string, mixed>|null supplied for foreach, only iterables are supported.
         🪪  foreach.nonIterable
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------

 [ERROR] Found 400 errors on changed lines (base: trunk) 

</details>

If you want I can start working on pushing up fixes for these.

#10 follow-up: @jorbin
4 hours ago

A couple of things that I am interested in seeing addressed:

  1. I would like to hear from the Woo folks on how they would use this and if it is something they think is valuable. (I see one of them is watching this ticket, but it still might be worth reaching out directly)
  1. How do you think the import and export plugins need to change to support this? Moving a website from one one host to another shouldn't break things.
  1. Is there any sort of alerting in place for if decryption fails or is the expectation that this is completely on implementors?

#11 @johnbillion
3 hours ago

  • Version trunk

#12 in reply to: ↑ 10 @ericmann
48 minutes ago

Replying to jorbin:

A couple of things that I am interested in seeing addressed:

...

  1. How do you think the import and export plugins need to change to support this? Moving a website from one one host to another shouldn't break things.

Export plugins should be able to pull the secrets if they _really_ want to in order to support a migration. But I'd personally advise against that approach as it would effectively write secrets in plain text into an export blob.

A better approach would be for the export plugins to define which secrets need to exist upon import to give the person doing the transfer the opportunity to set them on the new host first.

  1. Is there any sort of alerting in place for if decryption fails or is the expectation that this is completely on implementors?

This is why decryption failure results in a WP_Error rather than a false to differentiate between "this failed decryption" vs "this secret does not exist." Handling the failure itself would be on the implementors.

​@ericmann commented on ​PR #13759:


42 minutes ago
#13

I ran PHPStan rule level 10 on the changes in this PR and there were quite a few opportunities for improvement. For the changes in src:

[...]

If you want I can start working on pushing up fixes for these.

I appreciate the callouts sir! I need to get this resolved on the feature plugin as well to keep them in sync, so I'll start working on resolving this all ASAP.

Note: See TracTickets for help on using tickets.