Vaults
Configure Secured's entity storage system so global entities, thread entities, and runtime aliases are reused consistently.
Overview
The vault subsystem is the implementation behind Secured's entity storage system. It lets the SDK reuse known replacements instead of generating new ones every time.
At runtime, a vault can combine:
- a global encrypted snapshot from your backend
- optional thread-level encrypted snapshots
- SDK-managed runtime thread entries
- pending entries created before a thread is known
This gives you stable replacements such as keeping the same project codename or person alias across a thread, workspace, or product surface.
If your team talks about "global entities" or "stored entities", this is the system they belong to. In the current code, those stored entities are represented as VaultEntry records and managed by VaultManager.
Stored entity shape
Every stored entity record has the same shape after decryption:
interface VaultEntry {
original: string
replacement: string
type: string
}Example:
{
original: 'ProjectFalcon',
replacement: 'PROJECT_TOKEN',
type: 'CUSTOM',
}Global entities vs thread entities
Global entities
Global entities are workspace-level or product-level mappings fetched from your backend during initialization. They are the default persisted store for canonical replacements.
Thread entities
Thread entities are persisted mappings for one specific thread, conversation, or case. They override global entities when a thread context is active.
Runtime thread entities
If the SDK generates a new replacement while a thread is active, it stores that generated entity locally as a runtime thread entry so future obfuscation in that thread reuses the same alias.
Pending entities
If the SDK generates a new replacement before any thread is active, it stores it as a pending entity and later migrates it into the first thread you activate.
Configure a vault
import { PrivacyClient } from '@secured-ai/core'
const client = new PrivacyClient({
baseUrl: 'https://dev-api.securedai.com',
sdkAccessToken: import.meta.env.VITE_SECURED_SDK_ACCESS_TOKEN,
vault: {
repository: {
getGlobalSnapshot: async () => {
return null
},
getThreadSnapshot: async (threadId) => {
return null
},
},
masterKey: async () => 'vault-master-key',
},
})PrivacyVaultConfig
Prop
Type
masterKey and cacheNamespace can be async runtime values, which is helpful when auth or workspace context is not available at construction time.
Master keys
masterKey is the secret the client uses to decrypt persisted entity snapshots after they are fetched from your repository.
In practice:
- your backend returns encrypted global or thread snapshots
- the SDK fetches those snapshots
- the vault crypto layer decrypts them with
masterKey - the decrypted
VaultEntry[]records become available to replacement resolution
You can provide masterKey as:
- a plain string if it is already available
- an async runtime getter if it only becomes available after unlock, auth, or workspace selection
const client = new PrivacyClient({
baseUrl: 'https://dev-api.securedai.com',
sdkAccessToken: import.meta.env.VITE_SECURED_SDK_ACCESS_TOKEN,
vault: {
repository,
masterKey: async () => currentMasterKey,
},
})When the master key is not available yet
If masterKey resolves to null or is otherwise unavailable when the vault refreshes, persisted encrypted snapshots cannot be decrypted yet.
That means:
- global or thread entities may not be loaded into usable
VaultEntryrecords - the SDK falls back to generated replacements when needed
- once the key becomes available,
refreshVaultContext()should be called so snapshots can be retried and decrypted
await client.refreshVaultContext()Security model
The repository does not return plaintext entity mappings in the normal persisted flow. It returns encrypted snapshot payloads, and the client-side crypto layer decrypts them with masterKey.
The default crypto implementation is WebCryptoVaultCrypto, which uses browser Web Crypto APIs to encrypt and decrypt stored entries.
Rotating or rebinding master keys
If the active master key can change at runtime, provide it as a runtime value and call refreshVaultContext() after the change. This is the expected path when:
- a user unlocks the app and the key becomes available
- the active workspace changes
- the key depends on tenant-specific context
Repository contract
interface VaultRepository {
getGlobalSnapshot(
context: { cacheNamespace: string | null },
): Promise<EncryptedVaultSnapshot | null>
getThreadSnapshot?(
threadId: string,
context: { cacheNamespace: string | null },
): Promise<EncryptedVaultSnapshot | null>
}Snapshots are encrypted before they reach the client. The client decrypts them with the configured masterKey.
Default backend endpoints
If you use DefaultVaultRepository, it fetches persisted entity snapshots from:
GET /ai/sensitive-keysfor global entitiesGET /ai/threads/:threadId/sensitive-keysfor thread entities
These responses are expected to contain encrypted snapshot payloads, not plaintext entities.
DefaultVaultRepository uses the baseUrl configured on PrivacyClient; do not configure a second backend URL on the repository itself.
Replacement precedence
When obfuscate() needs a replacement, the vault checks the most specific layer first:
When a thread is active:
- runtime thread entries
- persisted thread snapshot
- global snapshot
- generated replacement pool fallback
When no thread is active:
- pending entries created before a thread is known
- global snapshot
- generated replacement pool fallback
This means thread-local consistency wins over global aliases, while pending entities only participate before a thread context exists.
How matching works
Stored entity matching is normalization-based rather than raw string label equality.
The system normalizes:
- original text by trimming and lowercasing
- entity types through canonical aliases such as
PER -> PERSON,LOC -> GPE, andORGANIZATION -> ORG
That lets persisted entities continue matching even when upstream detectors use slightly different but equivalent type labels.
Thread-aware flows
If your app has a conversation or case ID, set it before obfuscating:
await client.setThreadContext('thread-42')
const result = await client.obfuscate('ProjectFalcon launches tomorrow')New mappings created in that context become runtime thread entries and are reused for later obfuscation in the same thread.
If you obfuscate before a thread exists, entries are stored as pending and migrated into the first thread you set.
You can also scope a single obfuscation call without changing the globally active thread:
const result = await client.obfuscate('ProjectFalcon launches tomorrow', {
threadId: 'thread-42',
})Refreshing context
When auth, namespace, or workspace state changes, refresh the vault context:
await client.refreshVaultContext()This re-resolves runtime values like masterKey and cacheNamespace and reloads snapshots.
This is especially important when:
- the user has just unlocked the app and the master key becomes available
- the active workspace changed
- your
cacheNamespacedepends on tenant or workspace context
Inspecting vault state
The vault manager is available as client.vault.
const status = client.vault.getStatus()
const globalSnapshot = client.vault.getGlobalSnapshot()
const threadSnapshot = await client.vault.loadThreadSnapshot('thread-42')Helpful methods include:
getStatus()getGlobalSnapshot()getThreadSnapshot(threadId)loadThreadSnapshot(threadId)getRuntimeThreadEntries(threadId)getPendingEntries()clearCache()deobfuscate(text, options?)
getStatus() is useful for checking whether global entities are loaded, whether the current namespace has been resolved, and whether the cached global snapshot is stale.
Cache and namespace behavior
Vault snapshots are cached client-side. If you provide cacheNamespace, the cache is partitioned by that namespace so different workspaces do not bleed into each other.
If snapshot fetching fails but a cached copy exists, the vault can continue from cached data and mark it as stale.
Choosing a cache adapter
Secured exposes multiple cache adapters, and the right one depends on how durable you want entity storage to be:
MemoryVaultCachefor ephemeral in-memory usage and testsLocalStorageVaultCachefor persistence across reloads in the same browser profileSessionStorageVaultCachefor per-tab persistence that disappears when the tab/session closesIndexedDbVaultCachefor more durable async browser storage
If you do not provide a cache explicitly, the vault defaults to MemoryVaultCache.
import {
IndexedDbVaultCache,
LocalStorageVaultCache,
MemoryVaultCache,
SessionStorageVaultCache,
} from '@secured-ai/core'React integration
@secured-ai/react exposes this through useVault(), including helpers for setThreadContext() and refreshContext().