Declare the key your code needs. An operator creates the real key and binds it to that declaration. Your TypeScript only gets a handle; private bytes stay in the server.
Declare, create, bind
sessions.ts · declaration and methods
import { define, key, mutation, query } from "@flower-js/sdk";
import { jwt } from "@flower-js/sdk/crypto";
const sessions = key("sessions", {
algorithm: "Ed25519", usages: ["sign", "verify"],
});
const issue = mutation("session.issue", (ctx, user: string) => {
// Authenticate the caller and authorize this user before issuing a token.
if (typeof user !== "string" || !user) throw new TypeError("user required");
return jwt.sign({ sub: user, iss: "shop", aud: "shop-api",
exp: ctx.now() / 1000 + 900 }, sessions);
});
const check = query("session.check", (_ctx, token: string) =>
jwt.verify(token, sessions, { issuer: "shop", audience: ["shop-api"] }).claims);
export default define({ keys: [sessions], http: { issue, check } });- This example doesn’t authenticate callers. Add your own authorization before exposing
issue. - A declaration grants nothing on its own. An operation needs both the deployed declaration and an operator’s binding.
jwt.verifychecks the key version, expiry, issuer and audience.
An operator sets up the key:
Provision; then bind the declaration
# Install the same protected wrapping file on every authorized server.
umask 077
openssl rand 32 > /secure/flower-wrapping.key
# Start each server with FLOWER_KEYRING_FILE=/secure/flower-wrapping.key.
# SDK CLI: supply FLOWER_URL and FLOWER_ADMIN_TOKEN for your cluster.
flower key generate session-signing --algorithm Ed25519 --request-id create-session-key
flower key bind sessions session-signing --usages sign,verify --request-id bind-sessions
flower deploy sessions.ts --request-id deploy-sessions
flower key rotate session-signing --request-id rotate-sessions-2
flower key list
flower key cache- Every authorized server needs the same 32-byte wrapping file. Back it up separately.
- The default listener is unencrypted h2c. Enable native TLS to protect operator tokens.
- For a tenant database, add
--partition northto deploy and key commands, or useclient.partition("north").
To import an existing key, seal it locally first. Only the encrypted envelope is sent:
Import only when an existing identity is needed
# Native runtime executable: plaintext stays on this local machine.
/path/to/native/flower key seal --wrapping-key-file /secure/flower-wrapping.key \
--format pem < private.pem > sealed.json
# SDK CLI sends only the authenticated encrypted envelope.
flower key import imported-signing sealed.json --algorithm Ed25519 \
--request-id import-signingWhat each key can do
| Algorithm | Use it for |
|---|---|
| Ed25519 | JWT and NaCl signing and verification |
| P256, RSA, HS256 | JWT |
| A256GCM | Encrypted JWT |
| XSalsa20Poly1305, X25519 | Matching NaCl operations |
publicKey(handle)needs apublicKeygrant.nacl.box.before(peerPublic, privateHandle)returns a shared key you can use only inside that callback. It needs the derive and encrypt/decrypt grants.- Raw
Uint8Arraykeys still work for interoperability.
Randomness:
- Native randomness and automatic JWT nonces need a mutation. In a query, pass explicit unique nonces: 24 bytes for NaCl, 12 for AES-GCM.
nacl.setPRNGonly affects NaCl calls in your code, not key generation or JWT nonces. Each callback starts fresh, so a custom stateful generator can repeat outputs.
Rotate and revoke
- Rotate makes a new version for signing and encryption. JWTs carry their version in
kid, so old tokens still verify until that version is revoked. - NaCl has no
kid. SavekeyVersion(key).versionnext to each ciphertext, and decrypt withkeyVersion(key, savedVersion). That handle can’t sign or encrypt. - Revoke one version with
flower key revoke NAME --version N, or all versions by leaving out--version. After revoking the active version, rotate before new operations can run. - Retire keeps verification and decryption. keyDestroy removes the current encrypted material and leaves a tombstone. It does not erase backups.
- Declaring managed keys makes every query and watch in the app read from the quorum, including aliases marked replica-local.
- Revocation doesn’t cancel work that has already started. Results that depend on a revoked or locked key fail rather than go stale.
- Old versions stay stored (encrypted) until you garbage-collect them. Plan disk space.
- Private bytes never enter collections, bundles, responses or the Raft log.
Key cache and wrapping key
FLOWER_KEY_CACHE_BYTESsets the cache for prepared keys. Default 16 MiB; 0 disables it.FLOWER_KEY_CACHE_TTL_MSreloads cached keys after this age. Default 0 (never). It is not a guarantee that key material leaves memory by then.flower key cacheshows the cache on the node you connect to.
The wrapping file is read at startup; changes need a restart. To rotate it:
- Set the new file in
FLOWER_KEYRING_FILEand list the old ones inFLOWER_KEYRING_PREVIOUS_FILES(a JSON array). Restart. - Call
keyRewrapon every key catalog. - Remove the old files and restart again.
- When a partition moves, the destination must unlock every unrevoked version. A missing or wrong wrapping key pauses the move until fixed.
- Not supported yet: KMS/HSM backends and general exportable secrets.
- Your JS never sees private material, but the server holds it in memory.
See the managed-key reference for every method, import format, permission and limit, and the NaCl and JWT reference for all functions.