Publish a typed TypeScript SDK from this repo so another product (first: Tawny-SOC) can create, read, and show a Kelpie case without copying fetch or iframing /cases/[id].
Do not rewrite Kelpie. Do not add a fifth case store. The SDK wraps the live /api/v1 contract in docs/api.md and the Tawny push rules in docs/integrations/tawny.md.
Why
Tawny-SOC already talks to Kelpie from app/api/soc/actions/route.ts with untyped fetch and a Record. That client is local to Tawny-SOC. It is not this SDK.
The live Tawny-SOC handoff also does not match this repo's contract: it posts to /api/v1/alerts with externalRef / source: "Tawny-SOC" and PUTs cases with externalRef: tawny-case-*. Kelpie's Tawny guide wants POST /api/v1/cases with sourceSystem: "tawny", sourceReference, sourceUrl. Org always comes from the klp_* token, never the body. Retries are only safe when both source fields are present.
Kelpie already has MCP at /api/mcp for agents. This ticket is a library for product code, not another MCP.
What to ship
A package in this repo (for example packages/sdk or @kelpie/sdk) that Tawny-SOC, Bower-adjacent tools, and anything else can depend on.
import { createKelpieClient } from "@kelpie/sdk";
const kelpie = createKelpieClient({
baseUrl: process.env.KELPIE_URL,
token: process.env.KELPIE_TOKEN, // klp_...
});
const created = await kelpie.cases.create({
title: "Suspicious PowerShell on WEB-03",
summary: "Host WEB-03, user jsmith",
severity: "high",
classification: "malware",
tlp: "amber",
sourceSystem: "tawny",
sourceReference: "tawny_alert_7f3a9c21",
sourceUrl: "https://tawny.example.com/alerts/7f3a9c21",
});
// 201 created: true | 200 created: false, same id / caseNumber
Client rules
- Bearer
klp_* only. Empty scopes already grant nothing. Do not send organisationId in any body.
- Typed errors for
400 / 401 / 403 / 404 / 409 / 422, including created: false on idempotent create and closure_requirements_not_met.
- Compartment behaviour stays server-side. The SDK must not invent a second ACL. A 404 for a hidden case stays a 404.
- No browser cookie auth. Embedders use a server-side token.
Phase 1 methods (enough to embed a case pane)
These are the ones Tawny-SOC needs for Send / Promote / Open and a KelpieCasePane (number, status, assignee, last comment, SLA, link out):
cases.create → POST /api/v1/cases with optional sourceSystem / sourceReference / sourceUrl
cases.get → GET /api/v1/cases/{id}
cases.list → GET /api/v1/cases (status, severity, source=tawny, limit)
cases.update → PATCH /api/v1/cases/{id} (not status closed)
comments.list / comments.create on a case
observables.list / observables.create
Export a small read model for the pane: { id, caseNumber, status, assignee, severity, lastComment, sla, sourceUrl, kelpieUrl }.
Phase 2 (same package, later)
Wrap the rest of docs/api.md as needed: tasks, close/reopen, alerts, evidence, relationships, graph, ATT&CK. Do not block phase 1 on that.
Publish
- Workspace package with generated or hand-kept types that match
docs/api.md.
- README section: install, token scopes, Tawny create example, idempotency.
- Tests against the existing Tawny ingest scripts (
scripts/test-tawny-api.ts, scripts/test-tawny-ingest.ts) so create/replay still returns 201 then 200.
- Version independently if you publish to npm. First consumer can path-depend from git.
Embed, not iframe
Kelpie's /cases/[id] is a full IR app (SLA, queues, watchers, attack story, graph). Products that embed Kelpie should use this SDK and their own pane. Do not iframe Kelpie.
Tawny-SOC work (separate ticket on that repo): point Send / Promote at cases.create with sourceSystem: "tawny", drop the invented /api/v1/alerts send and the PUT. Hide Send once kelpieCaseId is set.
Token guidance for embedders
From docs/integrations/tawny.md:
- Push-only producer:
cases:write only.
- Pane that reads a case: add
cases:read and comments:read.
- Do not grant
cases:override_closure, audit:read, alerts:raw_payload:read, reports:admin, or reviews:admin to a product embed token.
Out of scope
- Changing Kelpie's REST or MCP.
- Merging Kelpie into Tawny-SOC.
- A Python or Go client in this ticket.
- Fixing Tawny-SOC's wire (that change lives in Tawny-SOC; it depends on this package).
Done when
@kelpie/sdk (name can flex) lives in this repo and can cases.create / get / list plus comments and observables against a running Kelpie.
- A replay with the same
sourceSystem + sourceReference returns the existing case (created: false), never a duplicate.
- Types reject a reserved
sourceSystem and a sourceReference without sourceSystem.
- Docs show how Tawny-SOC (or any embedder) constructs the client and the pane fields.
- No Clerk, no extra auth vendor.
klp_* only.
Publish a typed TypeScript SDK from this repo so another product (first: Tawny-SOC) can create, read, and show a Kelpie case without copying
fetchor iframing/cases/[id].Do not rewrite Kelpie. Do not add a fifth case store. The SDK wraps the live
/api/v1contract indocs/api.mdand the Tawny push rules indocs/integrations/tawny.md.Why
Tawny-SOC already talks to Kelpie from
app/api/soc/actions/route.tswith untypedfetchand aRecord. That client is local to Tawny-SOC. It is not this SDK.The live Tawny-SOC handoff also does not match this repo's contract: it posts to
/api/v1/alertswithexternalRef/source: "Tawny-SOC"andPUTs cases withexternalRef: tawny-case-*. Kelpie's Tawny guide wantsPOST /api/v1/caseswithsourceSystem: "tawny",sourceReference,sourceUrl. Org always comes from theklp_*token, never the body. Retries are only safe when both source fields are present.Kelpie already has MCP at
/api/mcpfor agents. This ticket is a library for product code, not another MCP.What to ship
A package in this repo (for example
packages/sdkor@kelpie/sdk) that Tawny-SOC, Bower-adjacent tools, and anything else can depend on.Client rules
klp_*only. Empty scopes already grant nothing. Do not sendorganisationIdin any body.400/401/403/404/409/422, includingcreated: falseon idempotent create andclosure_requirements_not_met.Phase 1 methods (enough to embed a case pane)
These are the ones Tawny-SOC needs for Send / Promote / Open and a
KelpieCasePane(number, status, assignee, last comment, SLA, link out):cases.create→POST /api/v1/caseswith optionalsourceSystem/sourceReference/sourceUrlcases.get→GET /api/v1/cases/{id}cases.list→GET /api/v1/cases(status,severity,source=tawny, limit)cases.update→PATCH /api/v1/cases/{id}(not statusclosed)comments.list/comments.createon a caseobservables.list/observables.createExport a small read model for the pane:
{ id, caseNumber, status, assignee, severity, lastComment, sla, sourceUrl, kelpieUrl }.Phase 2 (same package, later)
Wrap the rest of
docs/api.mdas needed: tasks, close/reopen, alerts, evidence, relationships, graph, ATT&CK. Do not block phase 1 on that.Publish
docs/api.md.scripts/test-tawny-api.ts,scripts/test-tawny-ingest.ts) so create/replay still returns 201 then 200.Embed, not iframe
Kelpie's
/cases/[id]is a full IR app (SLA, queues, watchers, attack story, graph). Products that embed Kelpie should use this SDK and their own pane. Do not iframe Kelpie.Tawny-SOC work (separate ticket on that repo): point Send / Promote at
cases.createwithsourceSystem: "tawny", drop the invented/api/v1/alertssend and thePUT. Hide Send oncekelpieCaseIdis set.Token guidance for embedders
From
docs/integrations/tawny.md:cases:writeonly.cases:readandcomments:read.cases:override_closure,audit:read,alerts:raw_payload:read,reports:admin, orreviews:adminto a product embed token.Out of scope
Done when
@kelpie/sdk(name can flex) lives in this repo and cancases.create/get/listplus comments and observables against a running Kelpie.sourceSystem+sourceReferencereturns the existing case (created: false), never a duplicate.sourceSystemand asourceReferencewithoutsourceSystem.klp_*only.