openlatch-provider 0.2.0

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
# Authoring a tool — Node.js (Express / Hono)

Same shape as the Python guide (`python.md`); language-specific notes
below.

## Scaffold

```bash
openlatch-provider new tool --template node --out my-tool
cd my-tool
pnpm install
```

## Minimum viable tool

```ts
import express from "express";
import crypto from "node:crypto";
import fs from "node:fs";

const WHSEC = fs.readFileSync(".secret", "utf8").trim();
const SECRET_BYTES = Buffer.from(WHSEC, "base64");  // strip whsec_ prefix first

const app = express();
app.use(express.raw({ type: "application/json", limit: "1mb" }));

app.post("/event", (req, res) => {
  const id = req.header("webhook-id")!;
  const ts = parseInt(req.header("webhook-timestamp")!, 10);
  const sig = req.header("webhook-signature")!;  // "v1,<b64>"

  if (Math.abs(Date.now() / 1000 - ts) > 300) {
    return res.status(401).send("timestamp skew");
  }

  const expected = crypto
    .createHmac("sha256", SECRET_BYTES)
    .update(`${id}.${ts}.`)
    .update(req.body as Buffer)
    .digest();
  const presented = Buffer.from(sig.split(",", 2)[1], "base64");
  if (!crypto.timingSafeEqual(expected, presented)) {
    return res.status(401).send("signature mismatch");
  }

  const event = JSON.parse((req.body as Buffer).toString());

  // ---- detection ----
  res.json({
    riskScore: 5,
    verdictHint: "allow",
    ruleId: null,
    rationaleSummary: null,
    latencyMs: 1,
  });
});

app.listen(8000);
```

## Why express.raw, not express.json

The HMAC covers the **raw body bytes**. If you parse with
`express.json` first, Node will re-serialize on access and the
signature will fail. Always work with `Buffer` until verification
succeeds.

## Hono variant

```ts
import { Hono } from "hono";
import { timingSafeEqual } from "node:crypto";
// ... same handler shape; Hono's c.req.raw lives on c.env.body.
```

## Per-action scoring + prior config state (v2)

Optional. Return a parallel `actions[]` array keyed by the deterministic
`actionRef` (`"{kind}:{index}"`, kind ∈ `domain|file|command`):

```ts
import { scoreToSeverity, type ActionScore } from '@openlatch/tool-sdk';

const risk = 87;
return {
  riskScore: risk,
  severityHint: scoreToSeverity(risk),
  verdictHint: 'deny',
  actions: [
    {
      actionRef: 'cmd:0',
      riskScore: risk,
      severity: scoreToSeverity(risk),      // MUST use the helper
      threatCategory: 'shell_dangerous',    // routing 12-category
      axes: { destructive: 18, exfil: 0, secret: 0, privesc: 0, reversibility: 20 },
    },
  ] satisfies ActionScore[],
};
```

- `actions` is optional, ≤256 items; omit it and per-action risk is
  recorded null (gap-tolerant).
- **`severity` must come from `scoreToSeverity()`** — the platform
  persists it verbatim, so first-party tools keep the canonical
  `<40 / 40-69 / 70-89 / 90+` buckets true by construction.
- Stateful config / tool-integrity detectors read prior artifact state
  from `event.priorconfigstate` — populated only when the capability
  declares `needs_prior_config_state: true` in `openlatch.yaml`.

## Best practices

- **`crypto.timingSafeEqual`** for the signature compare. Never `===`
  or `Buffer.compare` (both leak timing).
- **Body size cap**: enforce `1mb` at the framework level so a
  malicious body never grows past the runtime's own cap.
- **No PII in logs**. Your tool's logs flow into your own
  observability stack — keep the payload out and stick to outcome
  metadata.

## TypeScript SDK

`@openlatch/tool-sdk` (npm) wraps the verification + verdict shaping
in a typed handler:

```ts
import { handler } from "@openlatch/tool-sdk";

export const POST = handler({
  whsec: process.env.OPENLATCH_WHSEC!,
  detect(event) {
    return { riskScore: 5, verdictHint: "allow" };
  },
});
```

Returns a Web `Request -> Response` handler usable in Express, Hono,
Bun, and edge runtimes.