API
This chapter documents the public API of the nr-vault extension.
VaultService
The main service for interacting with the vault.
- interface VaultServiceInterface
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Service\ Vault Service Interface
Main interface for vault operations.
Note
The plaintext parameters
$secret/$newSecretcarry the PHP#[\SensitiveParameter]attribute on the interface, so they are redacted from stack traces andvar_dump()output. Mirror the attribute on any custom implementation.- store ( string $identifier, string $secret, array $options = []) : void
-
Store a secret in the vault.
$secretis a#[\SensitiveParameter].- param string $identifier
-
Unique identifier for the secret.
- param string $secret
-
The secret value to store (
#[\SensitiveParameter]). - param array $options
-
Optional configuration:
owner(int BE-user UID),groups(int[] BE-group UIDs),context(string),expiresAt(int|DateTimeInterface|null),metadata(array),description(string),scopePid(int). - throws ValidationException
-
If the identifier is invalid.
- throws AccessDeniedException
-
If the per-secret ACL or the operation permission refuses the write —
secret.createon a new secret,secret.rotateon an existing one, and additionallysecret.manage_policywhen the options change the owner or the group tiers. - throws EncryptionException
-
If encryption fails.
Operation permissions and the per-secret ACL are two independent gates. Holding one never implies the other, and both are asserted before the value is written.
- retrieve ( string $identifier)
-
Retrieve a secret from the vault.
- param string $identifier
-
The secret identifier.
- returntype
-
string|null
- throws AccessDeniedException
-
If user lacks read permission.
- throws SecretExpiredException
-
If the secret has expired.
- Returns
-
The decrypted secret value or null if not found.
- retrieveForFrontend ( string $identifier)
-
Frontend-scoped counterpart of
retrieve(). Only secrets flaggedfrontend_accessibleresolve here, and that requirement holds for every caller — including a request that happens to carry a backend session, whose ambient privileges would otherwise widenretrieve()'s access decision. Expiry, decryption, audit logging and read statistics behave as inretrieve(). See ADR-035: Per-request allow-set of frontend-resolvable identifiers.- param string $identifier
-
The secret identifier.
- returntype
-
string|null
- throws AccessDeniedException
-
If the secret is not frontend-accessible, or the actor lacks read permission.
- throws SecretExpiredException
-
If the secret has expired.
- Returns
-
The decrypted secret value or null if not found.
- exists ( string $identifier) : bool
-
Check if a secret exists.
- param string $identifier
-
The secret identifier.
- Returns
-
True if the secret exists.
- delete ( string $identifier, string $reason = '') : void
-
Delete a secret from the vault.
- param string $identifier
-
The secret identifier.
- param string $reason
-
Optional reason for deletion (logged).
- throws SecretNotFoundException
-
If secret doesn't exist.
- throws AccessDeniedException
-
If user lacks delete permission.
- assertDeletable ( string $identifier) : void
-
Assert that
delete()is permitted for this identifier — without deleting. Exists for callers that delete several secrets as one logical unit, such as a record delete spanning multiple vault fields: a vault delete is a hard delete with no restore, so a partially applied batch cannot be compensated, and the only way to keep it all-or-nothing is to run every permission gate up front and abort before the first deletion.A secret that does not exist returns without throwing — the goal state is already reached. Ask
exists()to distinguish absent from present. Passing does not guarantee the subsequent delete succeeds: an audit-write failure, or a permission revoked in between, can still abort it.- param string $identifier
-
The secret identifier.
- throws AccessDeniedException
-
If the current actor lacks delete permission.
- rotate ( string $identifier, string $newSecret, string $reason = '') : void
-
Rotate a secret with a new value.
$newSecretis a#[\SensitiveParameter].- param string $identifier
-
The secret identifier.
- param string $newSecret
-
The new secret value (
#[\SensitiveParameter]). - param string $reason
-
Optional reason for rotation (logged).
- throws SecretNotFoundException
-
If the secret does not exist.
- throws AccessDeniedException
-
If the per-secret ACL or the
secret.rotateoperation permission refuses it. - throws EncryptionException
-
If encryption fails.
- setEnabled ( string $identifier, bool $enabled, string $reason = '') : void
-
Enable or disable a secret — the single write path for its availability.
Disabling withdraws the secret from every read path at once: the record carries TCA's
disabledenable column, so a disabled secret resolves to nothing inretrieve,() retrieveand every placeholder that goes through them. It stays administrable — it can be re-enabled, rotated, deleted, and its metadata read — and reports its state inFor Frontend () Secret.Details::$enabled The state is absolute rather than a toggle: setting the state a secret already has is a no-op and writes no audit entry. A change is audited as
metadata_update, the same action the FormEngine path writes for the same column, and is all-or-nothing with that entry (ADR-036: Mutation and audit are all-or-nothing): if the audit write fails the previous availability is restored and the failure surfaces.- param string $identifier
-
The secret identifier.
- param bool $enabled
-
The availability the secret should have afterwards.
- param string $reason
-
Optional justification, recorded in the audit entry alongside the direction of the change.
- throws SecretNotFoundException
-
If the secret does not exist.
- throws AccessDeniedException
-
If the per-secret ACL or the
secret.manage_policyoperation permission refuses it.
- list ( ?string $pattern = null, bool $includeDisabled = false) : array
-
List accessible secrets.
- param string|null $pattern
-
Optional pattern to filter identifiers (supports the
*wildcard). - param bool $includeDisabled
-
Also return disabled secrets. Off by default, so a consumer asking which secrets are available keeps the answer it had; the management surfaces pass
true, because a disabled secret that never appears in a listing cannot be re-enabled.
- Returns
-
A
list<SecretMetadata>of secret metadata DTOs (Netresearch\NrVault\Domain\Dto\SecretMetadata); each entry reports its availability in$enabled.
- getMetadata ( string $identifier) : SecretDetails
-
Get metadata for a secret without retrieving its value. Resolves a disabled secret too — metadata is not the value, and withholding it would hide the record from the form that re-enables it.
- param string $identifier
-
The secret identifier.
- throws SecretNotFoundException
-
If secret doesn't exist.
- throws AccessDeniedException
-
If user lacks permission.
- Returns
-
A
SecretDetailsDTO (Netresearch\NrVault\Domain\Dto\SecretDetails) with identifier, description, owner, groups, version, availability ($enabled), etc.
EncryptionService
The crypto boundary: libsodium envelope encryption (per-secret DEK wrapped by the master key).
- interface EncryptionServiceInterface
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Crypto\ Encryption Service Interface
Low-level encryption operations. Most callers use
Vaultinstead.Service Interface Note
Plaintext and key parameters (
$plaintext,$encryptedValue,$encryptedDek,$oldMasterKey,$newMasterKey) carry the#[\SensitiveParameter]attribute on the interface.- encrypt ( string $plaintext, string $identifier) : EncryptedData
-
Encrypt a plaintext value with a unique DEK.
$plaintextis a#[\SensitiveParameter].- param string $plaintext
-
The value to encrypt (
#[\SensitiveParameter]). - param string $identifier
-
Secret identifier (used as AAD).
- throws EncryptionException
-
If encryption fails.
- Returns
-
An
EncryptedDatavalue object (Netresearch\NrVault\Crypto\EncryptedData) holding the ciphertext, encrypted DEK, and nonces.
- decrypt ( string $encryptedValue, string $encryptedDek, string $dekNonce, string $valueNonce, string $identifier, int $encryptionVersion = 1, string $encryptionAlgorithm = '') : string
-
Decrypt a previously encrypted value.
$encryptedValueand$encryptedDekare#[\SensitiveParameter].- param string $encryptedValue
-
Base64-encoded ciphertext (
#[\SensitiveParameter]). - param string $encryptedDek
-
Base64-encoded encrypted DEK (
#[\SensitiveParameter]). - param string $dekNonce
-
Base64-encoded DEK nonce.
- param string $valueNonce
-
Base64-encoded value nonce.
- param string $identifier
-
Secret identifier (used as AAD).
- param int $encryptionVersion
-
Stored per-secret encryption version. Defaults to
ENCRYPTION_VERSION_LEGACY(1), where the algorithm is derived from host capabilities. - param string $encryptionAlgorithm
-
Stored per-secret algorithm marker. Required for version 2+, must be
''for version 1. - throws EncryptionException
-
If decryption fails or the marker is unknown on this host.
- Returns
-
The decrypted plaintext.
- calculateChecksum ( string $plaintext) : string
-
Calculate a value checksum for change detection.
$plaintextis a#[\SensitiveParameter].- param string $plaintext
-
The secret value (
#[\SensitiveParameter]).
- Returns
-
SHA-256 hash (64 hex characters).
- reEncryptDek ( string $encryptedDek, string $dekNonce, string $identifier, string $oldMasterKey, string $newMasterKey, int $encryptionVersion = 1, string $encryptionAlgorithm = '') : ReEncryptedDek
-
Re-encrypt a DEK with a new master key (used during master-key rotation).
$encryptedDek,$oldMasterKeyand$newMasterKeyare#[\SensitiveParameter].- param string $encryptedDek
-
Current encrypted DEK (
#[\SensitiveParameter]). - param string $dekNonce
-
Current DEK nonce.
- param string $identifier
-
Secret identifier.
- param string $oldMasterKey
-
Previous master key (
#[\SensitiveParameter]). - param string $newMasterKey
-
New master key (
#[\SensitiveParameter]). - param int $encryptionVersion
-
Stored per-secret encryption version.
- param string $encryptionAlgorithm
-
Stored per-secret algorithm marker (required for version 2+).
The DEK is re-wrapped with the SAME algorithm the secret was encrypted with; the version and algorithm markers are unchanged by the operation.
- Returns
-
A
ReEncryptedDekvalue object (Netresearch\NrVault\Crypto\ReEncryptedDek).
EnvelopeCodec
Envelope encryption for a payload you keep in ONE column of your own table
(ADR-032). Use this instead of
Encryption when you have a blob rather than the vault's
seven-column layout.
- interface EnvelopeCodecInterface
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Crypto\ Envelope Codec Interface
- const MARKER
-
'nrv1:'— the version marker of the envelopessealproduces. A stored value is self-identifying, so a column can hold sealed and unsealed values during a migration.()
- seal ( string $plaintext, string $identifier) : string
-
Encrypt a payload into a single string.
$plaintextis a#[\SensitiveParameter].- param string $plaintext
-
The payload to protect (
#[\SensitiveParameter]). - param string $identifier
-
Context label bound to the ciphertext as additional authenticated data. Use a stable, per-purpose value (a column or use-case name), never a per-row one.
- throws EncryptionException
-
If encryption fails or the master key is unavailable.
- Returns
-
MARKER+ base64-encoded JSON envelope.
- open ( string $sealed, string $identifier) : string
-
Decrypt a sealed string. The stored change-detection checksum is not verified — integrity comes from the AEAD tag, which is always checked.
- param string $sealed
-
A string produced by
seal.() - param string $identifier
-
The SAME identifier the payload was sealed with.
- throws EnvelopeFormatException
-
If the string is not a well-formed envelope.
- throws EncryptionException
-
If authentication fails, the algorithm marker is unknown on this host, or the master key is unavailable.
- Returns
-
The decrypted payload.
- isSealed ( string $value) : bool
-
Whether a stored value is an envelope, as opposed to a plain value written before sealing was introduced.
- rewrap ( string $sealed, string $identifier, string $oldMasterKey, string $newMasterKey) : string
-
Re-wrap the envelope's DEK from one master key to another, leaving the payload ciphertext untouched — nothing is decrypted. This is the primitive behind
Foreign; you normally reach it throughEnvelope Rotator Interface Enveloperather than calling it directly.Rotation Context:: rewrap ()
Warning
seal wraps the payload's DEK with the CURRENT master key, and that
wrapped DEK lives in YOUR table where vault:rotate-master-key cannot reach
it. If you seal payloads you MUST also register a
Foreign (below), or your data becomes
permanently undecryptable the first time an operator rotates the master key.
ForeignEnvelopeRotator
How a consuming extension joins master-key rotation (ADR-033). Tag your implementation:
Vendor\Extension\Crypto\MyEnvelopeRotator:
tags: ['nrvault.foreign_envelope_rotator']
- interface ForeignEnvelopeRotatorInterface
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Crypto\ Foreign Envelope Rotator Interface
- getIdentifier ( ) : string
-
Short label naming your extension and the data it owns, for the operator-facing rotation report (e.g.
nr-llm: agent run state).
- getTables ( ) : array
-
Every table
rewrapwrites to. The command refuses to rotate when one of them is mapped to a different database connection thanAll () tx_nrvault_secret, because atomicity across two connections is a fiction.
- countEnvelopes ( ) : int
-
How many sealed envelopes you hold. Called outside the transaction, for the dry-run report and the operator summary. Throwing aborts the rotation before anything is touched.
- rewrapAll ( EnvelopeRotationContext $context) : int
-
Re-wrap every envelope you own; return how many. Runs INSIDE the vault's rotation transaction, after the vault's own secrets and before the commit.
Do not open, commit or roll back a transaction, and do not swallow failures: throwing rolls the ENTIRE rotation back, which is deliberate — a partial rotation leaves data wrapped under a key the operator has been told to destroy. Work in batches; the whole pass is one transaction.
- class EnvelopeRotationContext
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Crypto\ Envelope Rotation Context
Handed to
rewrap. It closes over the old and new master keys and exposes only the operation, so you move envelopes between keys without ever holding key material.All ()
SecretRedactor
The shared catalogue of recognisable secret shapes (ADR-031), used by this extension's plaintext scanner and available to any consumer that needs to mask secrets in log lines, error messages or outbound payloads.
This is a best-effort net for secrets that have already escaped their proper home. It recognises the catalogued shapes and nothing else, and is not a substitute for keeping secrets in the vault.
- interface SecretRedactorInterface
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Secret\ Secret Redactor Interface
- redact ( string $text, bool $includeEmails = false) : string
-
Replace every recognised secret occurrence in free text with a mask.
- param bool $includeEmails
-
Also mask e-mail addresses. Off by default: an address is personal data rather than a secret, and masking one inside, say, a model prompt changes what the text says.
- Returns
-
The masked text. If the regex engine gives up on a pathological input the text is returned as-is rather than emptied.
- isSecretIdentifier ( string $identifier, SecretIdentifierKind $kind) : bool
-
Whether a name reads as secret-bearing within its namespace. The kind matters: database columns and configuration keys are suffix-anchored, while environment variables use a broad substring rule. A name check alone is not enough —
GITHUB_PATsays nothing about being secret — so pair it withidentifyorValue () redacton the value.()
- enum SecretIdentifierKind
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Secret\ Secret Identifier Kind
DatabaseColumn,ConfigurationKey,EnvironmentVariable— the three identifier namespaces, deliberately not merged into one rule set.
Usage examples
Storing a secret
use Netresearch\NrVault\Service\VaultServiceInterface;
class MyService
{
public function __construct(
private readonly VaultServiceInterface $vault,
) {}
public function storeApiKey(string $apiKey): void
{
$this->vault->store(
'my_extension_api_key',
$apiKey,
[
'description' => 'API key for external service',
'groups' => [1, 2], // Admin, Editor groups
'context' => 'payment',
'expiresAt' => time() + 86400 * 90, // 90 days
]
);
}
}
Retrieving a secret
public function getApiKey(): ?string
{
return $this->vault->retrieve('my_extension_api_key');
}
Vault HTTP client
The vault provides a PSR-18 compatible HTTP client that can inject secrets
into requests without exposing them to your code. Configure authentication
with
with, then use standard
send.
Direct injection (recommended)
use GuzzleHttp\Psr7\Request;
use Netresearch\NrVault\Http\SecretPlacement;
use Netresearch\NrVault\Http\VaultHttpClientInterface;
final class ExternalApiService
{
public function __construct(
private readonly VaultHttpClientInterface $httpClient,
) {}
public function fetchData(): array
{
// Configure authentication, then use standard PSR-18
$client = $this->httpClient->withAuthentication(
'api_token',
SecretPlacement::Bearer,
);
$request = new Request('GET', 'https://api.example.com/data');
$response = $client->sendRequest($request);
return json_decode($response->getBody()->getContents(), true);
}
}
Via VaultService
use GuzzleHttp\Psr7\Request;
use Netresearch\NrVault\Http\SecretPlacement;
$client = $this->vaultService->http()
->withAuthentication('stripe_api_key', SecretPlacement::Bearer);
$request = new Request(
'POST',
'https://api.stripe.com/v1/charges',
['Content-Type' => 'application/json'],
json_encode($payload),
);
$response = $client->sendRequest($request);
- interface VaultHttpClientInterface
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Http\ Vault Http Client Interface
PSR-18 compatible HTTP client with vault-based authentication. Extends
\Psr\.Http\ Client\ Client Interface - withAuthentication ( string $secretIdentifier, SecretPlacement $placement = SecretPlacement::Bearer, array $options = []) : static
-
Create a new client instance configured with authentication. Returns an immutable instance - the original is unchanged.
- param string $secretIdentifier
-
Vault identifier for the secret.
- param SecretPlacement $placement
-
How to inject the secret.
- param array $options
-
Additional options (headerName, prefix, queryParam, bodyField, usernameSecret, reason).
- Returns
-
New client instance with authentication configured.
- withOAuth ( OAuthConfig $config, string $reason = 'OAuth2 API call') : static
-
Create a new client instance configured with OAuth 2.0 authentication.
- param OAuthConfig $config
-
OAuth configuration.
- param string $reason
-
Audit log reason.
- Returns
-
New client instance with OAuth configured.
- withReason ( string $reason) : static
-
Create a new client instance with a custom audit reason.
- param string $reason
-
Audit log reason for requests.
- Returns
-
New client instance with reason configured.
- withTimeout ( int $seconds) : static
-
Create a new client instance with a request timeout override. Applies Guzzle's
timeoutoption (total request duration) to every request sent through the returned instance, authenticated or not — use it for long-running API calls that exceed the instance-wide$GLOBALS. Connection establishment (['TYPO3_ CONF_ VARS'] ['HTTP'] ['timeout'] connect_timeout) stays platform-managed.- param int $seconds
-
Timeout in seconds; non-positive values mean "no override" and fall back to the platform default.
- Returns
-
New client instance with the timeout configured.
Cancelling an outbound request
PSR-18 returns a response, never a handle, so
send cannot be
aborted once it is running — a caller whose own work was cancelled still waits
out the timeout.
Cancellable adds a second send that
polls a caller-supplied signal and tears the socket down when it turns true.
It is a send, not an exported handle, and that is a security property rather
than a matter of taste. Four of this client's protections — the scheme
allowlist, the allowed_hosts gate, the credential injection and the audit
write — are statements inside the sending method, not middleware on the handler
stack, so they do not travel with a transport. The rule the package holds to is
therefore narrow and sharp: nothing hands you a client that already carries a
vault secret, and
Vault is the only place nr-vault attaches a
secret to your request. There is no send you can drive that puts a vault
secret on the wire without those four: every public method of that class returns
a configured clone, a PSR-7 response or a bool, and
send takes no options parameter — asserted by
VaultHttpClientCancellableTest::theCredentialBearingClientExportsNoTransportAndNoPromise().
nr-vault sends two credentials of its own on paths that are not your request and
do not carry all four: the X-Vault-Token header of the transit master-key
provider, on a plain Guzzle client, and the client_secret of the OAuth token
leg, which applies the allowed_hosts gate, writes one oauth_token_request
audit row per attempted round trip, and rides
send's
cancellation signal (issue #303). They are listed in
ADR-037: A cancellable send is a method, not an exported handle.
Building a hardened transport without vault credentials remains a supported,
public case:
Secure and
create return one, carrying the SSRF reject middleware and
the CURLOPT_RESOLVE DNS pin — and no secret, no allowlist gate, no audit
write.
The interface is separate from
Vault and purely
additive, so consumers feature-detect instead of raising a version floor:
use Netresearch\NrVault\Http\CancellableHttpClientInterface;
use Netresearch\NrVault\Http\CancellationSignalInterface;
use Netresearch\NrVault\Exception\RequestCancelledException;
final class RunCancellationSignal implements CancellationSignalInterface
{
public function __construct(private readonly MyRunState $run) {}
public function isCancelled(): bool
{
return $this->run->wasCancelled();
}
}
$client = $this->vaultService->http()
->withAuthentication('tool_api_key')
->withReason('MCP tool call')
->withTimeout(15);
try {
$response = $client instanceof CancellableHttpClientInterface && $client->supportsCancellation()
? $client->sendCancellable($request, new RunCancellationSignal($run))
: $client->sendRequest($request);
} catch (RequestCancelledException $e) {
// The call was abandoned on purpose; the audit log already recorded it.
}
The signal is polled once before the request is sent and then between ticks of
the transport event loop. Implementations must be cheap and must not throw:
the method is called several times a second per in-flight request, and it runs
after the credential has been injected. A signal that throws anyway still leaves
an audit row — the write is in a finally — but the exception reaches the
caller unchanged
(VaultHttpClientCancellableTest::aSignalThatThrowsMidFlightStillLeavesAnAuditRow()).
Note
Where a guarantee in this section names a test, that test is in
Tests/Unit/Http/VaultHttpClientCancellableTest.php or
Tests/Unit/Http/VaultHttpClientTest.php and enforces the sentence it is
named in; each fixed literal listed below is asserted by a test as well,
through the outcome-to-test table in
ADR-037: A cancellable send is a method, not an exported handle, which names one per row.
Two statements here name none and are descriptions rather than pinned
guarantees: what
Secure and
create hand out, and the invariant sentence introducing
this section — whose operative half, that no send you can drive skips the
four protections, does name one.
Note
When this client builds the transport itself, it comes from
Secure and carries the same hardened options, the
same SSRF/DNS-pin middleware
(ssrfDnsPinIsInstalledOnTheCancellableTransport()) and the same timeout
as the blocking client
(theTransportTheClientResolvesForItselfCarriesTheRememberedTimeout()) —
cancellation is an early exit, never an extension.
A PSR-18 client you injected into
Vault's constructor is
never replaced by a transport
(anInjectedGuzzleClientIsNeverSwappedForACancellableTransport()):
supports reports false for such an instance and
send completes the call blocking, on your client. The one
exception is
with, which has to bake the override into a
client and therefore rebuilds one from the factory — the clone it returns
reports
supports true
(withTimeoutRebuildsACallerSuppliedClientAndTurnsCancellationOn()).
A client obtained from
Vault supports
cancellation on any platform with curl_multi_*, through the withers
included (cancellationSurvivesTheWithersAndTheProductionFactory());
without that extension it degrades. Ask
supports rather than assuming either.
Every
send writes exactly one audit row — and so does every
send — so the log is complete with respect to calls and not
merely to egress. The outcome-to-test table in
ADR-037: A cancellable send is a method, not an exported handle is the enumeration that backs this
sentence: one row per way a call can end, one named test per row. Three actions
can result, and each means one thing — the
action is what you filter and count on, so none of them needs the error message
to be understood:
http_call_cancelled(badge: warning)- The signal stopped an in-flight request. The credential was retrieved,
injected and handed to the transport: treat it as exposed. Nothing else is
filed under this action, so "which calls were abandoned after their
credential went out?" is a query on this one value
(
theTwoCancellationOutcomesAreToldApartByTheirAction()). http_call_cancelled_before_send(badge: info)- The signal was already set when the send began. No secret was retrieved and
nothing was handed to the transport. Its own action so you can exclude these
rows by query rather than by reading messages
(
cancellingBeforeSendReadsNoSecretAndStillLeavesADistinguishableRow()). http_callwithsuccess = false- Everything that failed rather than was cancelled — a refused scheme or host, a transport that could not be built, a credential that could not be obtained, a transport error, the defensive wall-clock bound, a settlement that is not a response, or a throw from your signal or the ticker. Nobody asked for those, so they sit with the other failures. ADR-037 lists the test for each.
Within an action, the row's error message is a fixed literal shown under the badge:
Request cancelled before send: nothing egressed and no secret was retrieved- The pre-flight refusal.
Request cancelled after send began: credential injected and transfer handed to the transport- In the usual case the bytes are already out; if the signal turns true before the first tick they may not be, which is why the literal does not claim more than it can.
Cancellable transfer exceeded its wall-clock budget and was aborted- The defensive bound above the curl timeouts tripped, i.e. the transport stopped settling its promise.
Cancellable transport settled with a value that is not an HTTP response- The transfer settled with something unusable.
Cancellable transfer aborted by an unexpected error after the credential was injected: …- Guzzle's option handling, your signal or the ticker threw.
Blocking send aborted by an unexpected error after the credential was injected: …- The degraded blocking branch threw something that is not a PSR-18
ClientExceptionInterface. The same literal can appear for a plainsend, which runs the same send-and-audit helper.Request () Cancellable transport could not be built; nothing was sent: …- Building the transport for
sendthrew. It is built after the two guards and before the credential is read, so nothing egressed and no secret was retrieved. This one is specific toCancellable () send.Cancellable () Cancellable transfer was rejected- The promise rejected with a reason that is not a
Throwable, so there is no foreign message to append. Specific tosend.Cancellable () Request refused before any secret was read: unsupported URI scheme "…"- The URI was neither
httpnorhttps. The scheme is in the message because the audit context records method, host, path and status only. Request refused before any secret was read: host is not in the allowed hosts list$GLOBALS['TYPO3_CONF_VARS']['HTTP']['allowed_hosts']refused the destination. The host is on the row, in the context.Credential injection failed; nothing was sent: …- The vault read, or the OAuth token leg, threw. Nothing egressed.
The last three appear for
send as well: they are refusals of a
call that was asked for, and a call that was asked for shows up in the log.
The exception you receive is unchanged by that row — pinned by three
characterization tests in VaultHttpClientTest, written before the rows
existed.
See ADR-037: A cancellable send is a method, not an exported handle for the transport details and the residual gaps — in particular that the OAuth token round trip preceding an OAuth-authenticated call is not cancellable.
- interface CancellableHttpClientInterface
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Http\ Cancellable Http Client Interface
An outbound send that can be aborted while it is still on the wire. Implemented by
VaultalongsideHttp Client Vault.Http Client Interface - sendCancellable ( RequestInterface $request, CancellationSignalInterface $signal) : ResponseInterface
-
Send an HTTP request, aborting the transfer as soon as
$signalsays so. Runs the same guard sequence assend: scheme allowlist, host allowlist, credential injection, audit write.Request () Accepts no per-request transport options, deliberately — see ADR-037: A cancellable send is a method, not an exported handle.
When
supportsis false the call still completes, blocking, with an ordinaryCancellation () http_callaudit row. It degrades; it does not fail (aNonGuzzleInnerClientDegradesToABlockingSendWithAnOrdinaryAuditRow()).- param RequestInterface $request
-
PSR-7 request.
- param CancellationSignalInterface $signal
-
Polled before the send and between transport ticks.
- throws RequestCancelledException
-
If the signal aborted the call.
- throws ClientExceptionInterface
-
If the transfer itself failed.
- throws VaultException
-
If the scheme or host is rejected, or secret retrieval fails.
- Returns
-
PSR-7 response.
- supportsCancellation ( ) : bool
-
Whether this instance can abort a transfer in flight. False when the platform has no
curl_multi_*support, and false when the inner client was supplied by the caller instead of built bySecure— a supplied client may carry your own middleware or proxy, so it stays the one that sends. A pre-flight signal is still honoured in either case, because nothing has egressed yet, and the call is still audited (Http Client Factory aPreFlightSignalIsHonouredEvenWhenCancellationIsUnsupported()).
Authentication options
The
with method accepts these options:
- headerName
- Custom header name (for
Secret, default:Placement:: Header X-API-Key). - prefix
- Auth scheme/prefix prepended to the secret (for
Secret). Use for non-BearerPlacement:: Header Authorization: <scheme> <secret>schemes — e.g.'Key 'for the TYPO3 FAL providers or'DeepL-Auth-Key 'for DeepL. - queryParam
- Query parameter name (for
Secret, default:Placement:: Query Param api_key). - bodyField
- Body field name (for
Secret, default:Placement:: Body Field api_key). - usernameSecret
- Separate username secret identifier (for
Secret).Placement:: Basic Auth - reason
- Reason for access (logged in audit).
SecretPlacement enum
- placement
-
Authentication placement using
Secretenum:Placement Secret- Bearer token in Authorization header.Placement:: Bearer Secret- HTTP Basic Authentication.Placement:: Basic Auth Secret- Custom header value.Placement:: Header Secret- Query parameter.Placement:: Query Param Secret- Field in request body.Placement:: Body Field Secret- OAuth 2.0 with automatic token refresh.Placement:: OAuth2 Secret- X-API-Key header (shorthand).Placement:: Api Key
use GuzzleHttp\Psr7\Request;
use Netresearch\NrVault\Http\SecretPlacement;
// Bearer authentication
$client = $this->vault->http()
->withAuthentication('stripe_api_key', SecretPlacement::Bearer);
$response = $client->sendRequest(
new Request('POST', 'https://api.stripe.com/v1/charges', [], $body)
);
// Custom header
$client = $this->vault->http()
->withAuthentication('api_token', SecretPlacement::Header, [
'headerName' => 'X-API-Key',
]);
$response = $client->sendRequest(
new Request('GET', 'https://api.example.com/data')
);
// Custom Authorization scheme (e.g. DeepL "Authorization: DeepL-Auth-Key <key>")
$client = $this->vault->http()
->withAuthentication('deepl_api_key', SecretPlacement::Header, [
'headerName' => 'Authorization',
'prefix' => 'DeepL-Auth-Key ',
]);
$response = $client->sendRequest(
new Request('POST', 'https://api-free.deepl.com/v2/translate', [], $body)
);
// Basic authentication with separate credentials
$client = $this->vault->http()
->withAuthentication('service_password', SecretPlacement::BasicAuth, [
'usernameSecret' => 'service_username',
'reason' => 'Fetching secure data',
]);
$response = $client->sendRequest(
new Request('GET', 'https://api.example.com/secure')
);
// Query parameter
$client = $this->vault->http()
->withAuthentication('api_key', SecretPlacement::QueryParam, [
'queryParam' => 'key',
]);
$response = $client->sendRequest(
new Request('GET', 'https://maps.example.com/geocode')
);
PSR-14 events
The vault dispatches events during secret operations.
- class SecretCreatedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Secret Created Event
Dispatched when a new secret is created.
get: The secret identifier.Identifier () get: The Secret entity.Secret () get: User ID who created it.Actor Uid ()
- class SecretAccessedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Secret Accessed Event
Dispatched when a secret is read.
get: The secret identifier.Identifier () get: User ID who accessed it.Actor Uid () get: The secret's context.Context ()
- class SecretRotatedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Secret Rotated Event
Dispatched when a secret is rotated.
get: The secret identifier.Identifier () get: The new version number.New Version () get: User ID who rotated it.Actor Uid () get: The rotation reason.Reason ()
- class SecretDeletedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Secret Deleted Event
Dispatched when a secret is deleted.
get: The secret identifier.Identifier () get: User ID who deleted it.Actor Uid () get: The deletion reason.Reason ()
- class SecretUpdatedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Secret Updated Event
Dispatched when a secret value is updated (without rotation).
get: The secret identifier.Identifier () get: The new version number.New Version () get: User ID who updated it.Actor Uid ()
- class MasterKeyRotatedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Master Key Rotated Event
Dispatched by
vault:rotate-master-keyafter the rotation transaction has COMMITTED, so a listener never observes a rotation that was rolled back.get: Number of this extension's secrets re-encrypted.Secrets Re Encrypted () get: Number of consumer-owned envelopes re-wrapped (ADR-033).Foreign Envelopes Re Encrypted () get: The acting backend user, or 0 in a CLI context.Actor Uid () get: When the rotation completed.Rotated At ()
This is a notification, not a participation hook: a listener cannot re-wrap anything, because both master keys are gone by the time it runs. To have your own envelopes rotated, implement
Foreign.Envelope Rotator Interface
- class AuditIntegrityAlertEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Audit Integrity Alert Event
Dispatched when audit verification produces a finding.
get: The alert value object.Alert () get: The stable reason code (Reason () TABLE_RESET,EPOCH_DOWNGRADE,SINK_FAILURE,NO_EXTERNAL_SINK, …).is: Whether this finding is evidence of tampering rather than an availability problem — the discriminator a pager rule should key on.Tamper Evidence ()
- class BreakGlassActivatedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Break Glass Activated Event
Dispatched when a break-glass window opens.
get/Actor Uid () get: Who opened it.Actor Username () get: The mandatory justification.Reason () get: When the window lapses on its own.Expires At ()
- class BreakGlassDeactivatedEvent
-
- Fully qualified name
-
\Netresearch\
Nr Vault\ Event\ Break Glass Deactivated Event
Dispatched when a window is closed deliberately. A window that merely expires dispatches nothing — nothing runs at the moment it lapses, so reconstruct the closed interval from the activation event's expiry.
get/Actor Uid () get: Who closed it.Actor Username () get: The closing note.Reason ()