Skip to content

Audit & security

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

Audit log: every write recorded with who, when and what

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 tenant
defineFeature("ops", (r) => {
r.queryHandler(
"recent-audit",
z.object({}),
async (_query, ctx) =>
ctx.query(AuditQueries.list, {
limit: 10,
}),
{ access: { roles: ["Admin"] } },
);
});

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.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.

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",
});
r.writeHandler(chargeWrite);
});
}

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 },
},
);
});
}