@mikku.dev/sdk
v0.2.0
Published
Official TypeScript SDK for mikku.dev
Readme
@mikku.dev/sdk
Official TypeScript SDK for mikku.dev — a self-hosted
message queue for HTTP, with durable workflows. Zero runtime dependencies; uses
the platform fetch and crypto.subtle, so it runs in Node 20+, Bun, Deno,
Cloudflare Workers, and the browser.
npm install @mikku.dev/sdkClient
import { MikkuClient } from "@mikku.dev/sdk";
const mikku = new MikkuClient({
apiKey: process.env.MIKKU_API_KEY!,
baseUrl: "https://mikku.dev", // or your self-hosted URL
signingSecret: process.env.MIKKU_SIGNING_SECRET, // optional default
});
// publish to a single URL (or a fanout)
await mikku.publish({
url: "https://api.example.com/hook",
body: { hello: "world" },
delaySeconds: 10,
maxAttempts: 5,
backoffStrategy: "exponential",
idempotencyKey: "order_123",
});
// schedules, fanouts, message introspection
await mikku.createSchedule({ name: "digest", cron: "0 * * * *", url });
await mikku.createFanout({ name: "events", endpoints: [url1, url2] });
await mikku.getMessage(id);
await mikku.listMessages({ status: "failed" });
await mikku.resendMessage(id);Verifying deliveries
The worker signs each delivery with HMAC-SHA256 when a signingSecret is set.
Verify it on the receiving end with the raw body (re-serializing parsed JSON can
change bytes and break the HMAC):
import { verifySignature } from "@mikku.dev/sdk/signing";
export async function POST(req: Request) {
const body = await req.text(); // raw bytes — don't JSON.parse before verify
const ok = await verifySignature({
secret: process.env.MIKKU_SIGNING_SECRET!,
body,
signature: req.headers.get("mikku-signature")!,
timestamp: req.headers.get("mikku-timestamp")!,
messageId: req.headers.get("mikku-message-id")!,
});
if (!ok) return new Response("invalid signature", { status: 401 });
const payload = JSON.parse(body);
// … handle payload
}@mikku.dev/sdk/verify also exports verifyMikkuRequest(req, { secret })
(secret may be an array during rotation), which reads the body for you, returns
{ body, messageId, timestamp }, and throws SignatureVerificationError on a
bad signature.
Durable workflows
Define a workflow as your own HTTP endpoint. Mikku invokes it once per step and replays completed steps from a memoization log, so the function can sleep, wait for signals, and resume with its state intact across crashes and redeploys.
import { workflow, serve } from "@mikku.dev/sdk/workflow";
const orderFlow = workflow("order-flow", async (ctx) => {
const charge = await ctx.step.run("charge", () => stripe.charge(ctx.input));
ctx.state.chargeId = charge.id;
await ctx.step.sleepUntil("cooldown", Date.now() + 7 * 864e5);
const review = await ctx.step.waitForEvent<{ ok: boolean }>("review", {
timeout: "30d",
});
const [a, b] = await ctx.step.all([
{ id: "notify", run: () => notify(ctx.input.user) },
{ id: "ship", run: () => ship(ctx.input.order) },
]);
const receipt = await ctx.step.startChild("receipt", "receipt-flow", {
orderId: ctx.input.order,
});
return { ok: review?.ok ?? false, receipt };
});
// Web-standard handler — Next.js App Router, Bun, Deno, Cloudflare Workers
export const POST = serve([orderFlow], {
signingSecret: process.env.MIKKU_WORKFLOW_SECRET,
});Start and drive runs from the client:
const { id } = await mikku.startWorkflow({
workflow: "order-flow",
url: "https://app.example.com/api/workflows",
input: { user: "u_1", order: "o_42" },
signingSecret: process.env.MIKKU_WORKFLOW_SECRET,
});
await mikku.signalWorkflow(id, "review", { ok: true });
const { run, history } = await mikku.getWorkflowRun(id);
await mikku.cancelWorkflow(id);Step API
| Call | What it does |
| ------------------------------------------ | ------------------------------------------------------------- |
| ctx.step.run(id, fn) | Run a side effect exactly once; memoized on replay. |
| ctx.step.sleep(id, ms) / sleepUntil(id, date) | Durable sleep, surviving restarts. |
| ctx.step.waitForEvent(name, { timeout? })| Park until a signal arrives; payload, or null on timeout. |
| ctx.step.all([{ id, run }]) | Run steps concurrently, checkpointed as one batch. |
| ctx.step.startChild(id, name, input?) | Start a child workflow and await its result. |
| ctx.continueAsNew(input?) | Restart with fresh history — bounds growth in long loops. |
| ctx.state | JSON blob loaded before each invocation, saved each checkpoint. |
Step ids (and event/child names) must be unique within a run — they're the
replay keys. Steps are at-least-once; wrap non-idempotent effects accordingly.
Workflows are version-pinned via workflow("name", fn, { version }), so a
redeploy that changes step order won't corrupt in-flight runs.
License
MIT
