@seal-dev/sdk
v0.2.0
Published
Client SDK for the Seal approval API: gate.require(), resume(), and verifyWebhook().
Maintainers
Readme
@seal-dev/sdk
Client SDK for the Seal approval API. Wrap a risky agent action in a human approval gate with a single call.
- ESM + CJS, ships type declarations, tree-shakeable (
sideEffects: false). - The
require/resumepath uses onlyfetch— runs on Node ≥18 and edge runtimes. verifyWebhookusescrypto; import it only where a Node/WebCrypto runtime is available (tree-shaken out of the client path when unused).
Install
npm install @seal-dev/sdkGate an action
import { createGate, ApprovalRejectedError } from "@seal-dev/sdk";
const gate = createGate({
apiKey: process.env.SEAL_API_KEY!,
baseUrl: "https://api.useseal.dev",
});
try {
const approval = await gate.require({
action: "refund.issue",
payload: { customerId: "cus_123", amountCents: 5000 },
context: { reason: "Customer reported a duplicate charge." },
policyKey: "refunds",
});
// resolves only once approved (auto or by a human reviewer)
await issueRefund(approval.payload);
} catch (err) {
if (err instanceof ApprovalRejectedError) {
console.log("Reviewer rejected:", err.feedback);
} else {
throw err;
}
}require() resolves when the request is approved, and throws a typed error otherwise:
| Outcome | Thrown |
| ---------------------------- | -------------------------------------------- |
| rejected | ApprovalRejectedError (carries feedback) |
| expired | ApprovalExpiredError |
| still pending at timeoutMs | ApprovalPendingError |
Options
gate.require(input, {
waitSeconds: 30, // per long-poll cycle (server caps at 30)
timeoutMs: 300_000, // overall budget; omit to poll until a decision
poll: false, // return the pending handle instead of waiting (use with webhooks/resume)
signal, // AbortSignal
});Ask a human (human-as-tool)
When the agent needs a human's judgement as information — not a yes/no gate — use gate.ask().
It resolves to the exact text the reviewer typed once they answer (in the dashboard or Slack).
import { createGate, ApprovalRejectedError } from "@seal-dev/sdk";
const { answer } = await gate.ask({
question: "What's the customer's refund reason?",
policyKey: "asks", // a policy that routes questions to human reviewers
context: { ticketId: "T-4821" },
});
// `answer` is the reviewer's free text.ask() resolves once answered; a declined question throws ApprovalRejectedError, and an unanswered
one follows the same expiry/timeout errors as require() — it never fabricates an answer.
Resume a suspended workflow
const current = await gate.resume(approvalId);Verify a webhook
import { verifyWebhook, WebhookVerificationError } from "@seal-dev/sdk";
app.post("/webhooks/seal", async (req, res) => {
const raw = await readRawBody(req);
try {
const event = verifyWebhook(raw, req.headers, process.env.SEAL_WEBHOOK_SECRET!);
// event.type: "approval.approved" | "approval.rejected" | ...
} catch (err) {
if (err instanceof WebhookVerificationError) return res.status(400).end();
throw err;
}
});Signature scheme and header names are shared verbatim with the server, so verification always matches issuance.
