Vault

createVault, its methods, and the secret store it keeps encrypted records in.

What the vault is for and how to use it: Secrets (the vault).

ts
import { createVault } from "@intentface/latch-core";
import { createPostgresSecretStore } from "@intentface/latch-drizzle";

const vault = createVault<Principal>({
  store: createPostgresSecretStore(db, { owner: (p) => (p.userId ? [p.orgId, p.userId] : [p.orgId]) }),
  masterKey: process.env.LATCH_VAULT_KEY!, // base64 of 32 random bytes
});

createVault throws if a key isn't 32 bytes, or a key id isn't letters, digits, _ and - (up to 32), or two keys share an id.

Option
storeWhere the encrypted records live (below).
masterKeyThe key new secrets are saved under: base64 of 32 random bytes, { id, key }, or a VaultKeyProvider that calls your key service. A plain string has the id default.
previousKeysKeys, in either form, that still read secrets but never save new ones. Keep the old key here while rotating.

Each saved secret records the id of the key it was saved under, in wrappedDek. A secret is read with that key when it's configured, and otherwise with whichever configured key held here (not a key service) opens it. Rotating is described in Secrets.

A key in a key service (example in Secrets):

OptionTypeDefaultDetails
idstring—
wrapfunction—
unwrapfunction—
OptionTypeDefaultDetails
contextRecord<string, string>—
aadUint8Array<ArrayBufferLike>—

A record whose key id isn't configured is tried against the keys held here, never against a key service.

deriveMasterKey(passphrase, { salt, cost? }) from @intentface/latch-core/node derives a key from a passphrase (Node only). salt must be stable for the deployment. cost is scrypt's N, a power of two from 2 ** 14 (the default, kept so existing keys don't change) to 2 ** 20; use 2 ** 17 for a new deployment. A different cost gives a different key.

Methods

OptionTypeDefaultDetails
rotatefunction—
set(principal: P, key: string, value: string) => Promise<void>—
get(principal: P, key: string) => Promise<string | undefined>—
has(principal: P, key: string) => Promise<boolean>—
delete(principal: P, key: string) => Promise<void>—
list(principal: P) => Promise<string[]>—
resolverSecretResolver<P>—

createVault returns a RotatableVault. Tools and connections take a Vault, the same without rotate, so a wrapper of your own around a vault doesn't need to implement it.

Secret store

createPostgresSecretStore(db, { owner }) and createSqliteSecretStore(db, { owner }) keep the encrypted records in latch_secrets. owner(principal, key?) returns the owner's segments, and receives the secret's key so different kinds of secret can belong to different owners. list has no key to pass, so it lists the owner owner(principal) gives without one.

They also implement ownerKeyOf, scan and replaceWrap, so secrets are tied to their owner and vault.rotate() works. A store of your own implements:

OptionTypeDefaultDetails
put(principal: P, record: SecretRecord) => Promise<void>—
get(principal: P, key: string) => Promise<SecretRecord | null>—
delete(principal: P, key: string) => Promise<void>—
list(principal: P) => Promise<string[]>—
purgeOwner?function—
ownerKeyOf?((principal: P, key: string) => string)—
scan?function—
replaceWrap?function—

It only ever handles encrypted records; values are encrypted and decrypted in createVault. Without ownerKeyOf, secrets are tied to their name only, so a record copied into another owner's row would still open there. Without scan and replaceWrap, rotate() throws UnsupportedOperationError.