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 EncryptionException
-
If encryption fails.
- 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.
- 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.
- 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).
- list ( ?string $pattern = null) : array
-
List accessible secrets.
- param string|null $pattern
-
Optional pattern to filter identifiers (supports the
*wildcard).
- Returns
-
A
list<SecretMetadata>of secret metadata DTOs (Netresearch\NrVault\Domain\Dto\SecretMetadata).
- getMetadata ( string $identifier) : SecretDetails
-
Get metadata for a secret without retrieving its value.
- 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, 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.
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 get: Number of secrets re-encrypted.Secrets Re Encrypted () get: User ID who performed the rotation.Actor Uid () get: DateTimeImmutable of when rotation completed.Rotated At ()