Audit & security
Three features for compliance, secret management and protection against runaway workloads.

Status: ✅ Stable
What: Every write through the pipeline (create/update/delete) is recorded for compliance review, who, when, what, with before/after context. Comes out of the box; your handlers don’t need to know about it.
How it works: Registered as a postSave system hook. Receives the
SaveContext from the lifecycle pipeline and appends an audit event.
Operators browse via the built-in audit screens, or call
AuditQueries.list / AuditQueries.details from another handler.
Example:
import { createAuditFeature, AuditQueries } from "@cosmicdrift/kumiko-bundled-features/audit";import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";import { z } from "zod";
await runDevApp({ features: [createAuditFeature(), myFeature],});
// Custom query: recent audit rows for the current tenantdefineFeature("ops", (r) => { r.queryHandler( "recent-audit", z.object({}), async (_query, ctx) => ctx.dispatch.query(AuditQueries.list, { limit: 10, }), { access: { roles: ["Admin"] } }, );});secrets
Section titled “secrets”Status: ✅ Stable
What: Per-tenant encrypted values, API keys for external
services, OAuth client secrets, webhook tokens. Envelope-encrypted at
rest under a platform master key (KUMIKO_SECRETS_MASTER_KEY_V1, with
versioned successors for rotation).
How it works: r.secret(name, opts) (or the object form
r.secret({ name, … })) declares a typed secret handle. Operators set
values via secrets:write:set. In handlers:
const branded = await ctx.services.secrets.get(tenantId, handle, auditCtx?) :
then branded.reveal() for the plaintext. Prefer
requireSecretsContext(ctx, handlerName) so every .get carries the
audit user + handler automatically. Reads append a tenantSecretRead
event.

The secretsEdit screen type lists the declared keys for a tenant admin.
Each row shows whether a value is set; saved values are never shown again.
Writes: secrets:write:set and secrets:write:delete only accept keys
that a feature declared with r.secret, or keys under a declared
r.secretNamespace. Any other key fails with a not-found error, so a tenant
admin cannot store arbitrary rows next to the feature’s own keys. Two options
narrow writes further:
writeRoleslists the roles that may set or delete the key. The caller needs the handler access and one of these roles. An empty list is a boot error. WithoutwriteRoles, the handler access alone decides.valueSchemais a Zod string schema that every new value must pass. Bothsecrets:write:setandctx.services.secrets.setcheck it. A failed check returns a generic validation error on thevaluefield that contains neither the value nor the schema’s issues. Values that are already stored are not checked again.
r.secretNamespace takes the same two options for every key in the namespace.
To catch a rejected value from ctx.services.secrets.set in your own code, use
isInvalidSecretValueError(err) from
@cosmicdrift/kumiko-bundled-features/secrets. It matches on the error code and
i18n key, so it also works across a duplicated framework copy, where
instanceof would fail.
Rotation: a built-in rotate job re-encrypts envelopes after a KEK
version bump (KUMIKO_SECRETS_MASTER_KEY_V2, … +
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION).
secrets vs config:
config is for settings operators maintain (SMTP host from a settings
dialog); secrets is for external credentials with rotation and
read-audit requirements (Stripe API key). When a compliance auditor asks
“who accessed the Stripe key when”, that’s secrets.
Recipe: samples/recipes/secrets-demo
Example:
import { createSecretsFeature, requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";import { defineFeature, defineWriteHandler, type SecretKeyHandle } from "@cosmicdrift/kumiko-framework/engine";import { z } from "zod";
await runDevApp({ features: [createSecretsFeature(), createBillingFeature()],});
let STRIPE_API_KEY: SecretKeyHandle;
const chargeWrite = defineWriteHandler({ name: "charge", schema: z.object({ amount: z.number().positive() }), access: { roles: ["TenantAdmin"] }, handler: async (event, ctx) => { const secrets = requireSecretsContext(ctx, "billing:write:charge"); const branded = await secrets.get(event.user.tenantId, STRIPE_API_KEY); const apiKey = branded!.reveal(); // use apiKey in-memory only, never return it to the client return { isSuccess: true, data: { charged: event.payload.amount } }; },});
export function createBillingFeature() { return defineFeature("billing", (r) => { STRIPE_API_KEY = r.secret("stripe.apiKey", { label: { en: "Stripe API Key" }, scope: "tenant", writeRoles: ["TenantAdmin"], valueSchema: z.string().startsWith("sk_"), }); r.writeHandler(chargeWrite); });}rate-limiting
Section titled “rate-limiting”Status: ✅ Stable
What: Per-handler throttle (L3) wired by the dispatcher when a handler
declares rateLimit. Optional ops introspection via
rate-limiting:query:status. Protects login, expensive search, and
other hot paths.
How it works: Sliding window (typically Redis-backed when configured
on the server). Declare limits as
{ per: "user" | "tenant" | "ip" | "user+handler" | "tenant+handler" | "ip+handler", limit, windowSeconds } on the
handler options. The rate-limiting feature itself is optional if you
only need the L3 option and no status query.
Recipe: samples/recipes/rate-limiting
Example:
import { createRateLimitingFeature } from "@cosmicdrift/kumiko-bundled-features/rate-limiting";import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";import { z } from "zod";
await runDevApp({ features: [createRateLimitingFeature(), createRateLimitShowcaseFeature()],});
export function createRateLimitShowcaseFeature() { return defineFeature("rl-showcase", (r) => { r.queryHandler( "expensive-search", z.object({ q: z.string().min(1) }), async ({ payload }) => ({ q: payload.q, hits: 0 }), { access: { roles: ["Admin", "User"] }, rateLimit: { per: "user", limit: 3, windowSeconds: 60 }, }, ); });}