Choosing a backend
keystorePath and keystore are mutually exclusive. Passing both throws.
The file store
keystorePath is shorthand for keystore: fileKeystore(path), and you can pass the adapter directly if you prefer to be explicit:
- Atomic writes. Contents go to a
.tmpsibling in the same directory and are renamed over the target. A crash, a full disk, or a power loss mid-write leaves either the intact previous file or the intact new one, never a truncated keystore. 0600permissions. Only the owning user can read it.- A missing file means first run. Any other read failure (
EACCES,EISDIR,EIO) propagates instead of being mistaken for a first run.
The environment-variable store
For hosts with no writable disk. The value is read from an environment variable, and every write hands you a base64 blob to persist wherever your platform keeps secrets.string
default:"CLOAK_KEYSTORE"
The environment variable to read and write. The default is exported as
DEFAULT_KEYSTORE_ENV.(blob: string) => void | Promise<void>
Called with the base64 blob after every write. This is what makes the environment store durable. Without it the identity dies with the process.
onWrite captures the blob, confirm the blob landed in your secret store, then restart and confirm the bot reuses it rather than warning again.
Writing your own adapter
An adapter is three methods.describe() is optional and only supplies a label for operator-facing error messages.
The one rule
A failure that masquerades as a first run makes the SDK generate a brand new identity. Publication is write-once server-side, so that identity can never be published, and the bot is permanently broken through no fault of its own. Rejecting is always the safe answer: a bot that will not start is recoverable, a bot that regenerated its identity is not. Concretely, if you are wrapping a store that has a “not found” concept:- “not found” (a 404,
ENOENT, a missing row) maps tonull. - Everything else throws. Including “I could not tell”.
An HTTP secret-store adapter
This one talks to a secret service overfetch, so it needs no extra packages. Note how narrowly null is produced.
secretKeystore.ts
bot.ts
Things to get right
Make writes atomic, or at least all-or-nothing
Make writes atomic, or at least all-or-nothing
A reader must never observe half a keystore. A store with a single-key put gives you this for free. If yours does not, write to a staging key and swap.
Do not cache reads
Do not cache reads
read() is called once, before any network traffic and before any identity decision. Caching buys nothing and risks answering from a stale copy after an operator restores a backup.Do not swallow write failures
Do not swallow write failures
Throw. The SDK decides what a failed save means, and it handles that decision in one place. An adapter that silently drops a write turns a loud problem into a quiet one.
Treat the contents as a secret, not as config
Treat the contents as a secret, not as config
The keystore holds live conversation keys. Anyone who can read it can decrypt your bot’s traffic. Use a secret store, not a config map, and keep it out of logs.
Return something useful from describe()
Return something useful from describe()
It is the label an operator sees when the SDK reports a keystore problem.
secret store https://vault.internal/... is worth more at 3am than the default.Backup and restore
Back the keystore up the way you would back up a private key, because it contains one.- Back it up after the first successful login, once the identity is published. That is the copy that matters.
- Restore is a straight overwrite: put the bytes back where the adapter reads them.
- The environment store’s blob is base64 of the same JSON, so a file backup and an environment backup are interconvertible.
Next
Keys and the keystore
What the keys are, and why publication is write-once.
Deploying a bot
Where the keystore fits in a real deployment.
When a bot will not start
Symptom-indexed triage, including keystore failures.
Known limitations
The honest list of what does not work yet.