Skip to content

Node.js SDK

@vulnify/sdk asks Vulnify whether an action is allowed before your code runs it. These pages match version 0.3.0. The client behavior below is the 0.2.3 client. 0.3.0 adds the vulnify CLI in the same package. The package requires Node.js 18 or newer and ships ESM and CommonJS builds.

Terminal window
npm install @vulnify/sdk

Source and changelog: github.com/vulnify/vulnify-sdk. Package: npmjs.com/package/@vulnify/sdk.

0.2.0 was not published. That tag still treats some 4xx responses as outages. The fix shipped in 0.2.1. Install 0.3.0.

import { Vulnify } from '@vulnify/sdk';
const vulnify = new Vulnify({
apiKey: process.env.VULNIFY_API_KEY!,
// Optional. The constructor does not read the environment itself.
// Leave this unset to use https://api.vulnify.io. Set VULNIFY_BASE_URL for another host.
baseUrl: process.env.VULNIFY_BASE_URL,
timeoutMs: 3000,
failMode: 'closed',
retries: 2,
});
Option Default Meaning
apiKey required Sent as Authorization: Bearer.
baseUrl https://api.vulnify.io Pass baseUrl, commonly process.env.VULNIFY_BASE_URL, for another host. A trailing slash is stripped.
timeoutMs 3000 Per attempt.
failMode 'closed' Used only for timeouts, network errors, 408, 429, and 5xx.
retries 2 Extra attempts after the first. The same Idempotency-Key is reused.

check() posts to /v1/events. Timeouts, network errors, 408, 429, and 5xx do not throw. They return the fallback decision after retries. Every other 4xx throws, including 400, 401, 403, 404, and 413, whether or not failMode is 'open'. A 413 means the JSON body is over 200 KB. It is a client error. The SDK throws Error and does not retry. Fail-open does not turn it into ALLOW.

const decision = await vulnify.check(
{
agent: 'SalesBot',
action: 'EXPORT_DATA',
resource: 'Customer Database',
destination: 'EXTERNAL_EMAIL',
recordsAffected: 12000,
// content is optional. It is scanned and not stored.
},
{ idempotencyKey: 'optional-stable-key' },
);

Omit idempotencyKey and the SDK generates a UUID. Pass your own key when you want a retry of the same logical check to hit the same event.

action is one of READ_DATA, WRITE_DATA, DELETE_DATA, EXPORT_DATA, SEND_EMAIL. destination is INTERNAL, EXTERNAL_EMAIL, or EXTERNAL_API. Name the agent with agent or agentId, and the resource with resource or resourceId.

Run the action only when finalDecision is ALLOW. check() and getEvent() require finalDecision. Idempotent replays include it. The unreachable-API fallback sets it too.

const outcome = decision.finalDecision;
if (outcome === 'ALLOW') {
await exportCustomerRecords();
} else if (outcome === 'REVIEW') {
// Stop. A person still has to approve. decision.id is the event to poll.
} else if (decision.degraded) {
// Vulnify could not be reached. finalDecision is BLOCK unless failMode is open.
} else {
// BLOCK. A denied or expired review lands here even though decision stays REVIEW.
}
finalDecision Meaning
REVIEW A review is pending. Do not run the action.
ALLOW The action may run. After a person approves, the stored decision is still REVIEW.
BLOCK Do not run the action. Denial and expiry both set this.

decision is the outcome recorded on the event. It does not change when a review is resolved. When there is no review, finalDecision equals decision. The other fields are documented in Decisions. degraded is set by the SDK, not by a successful API response.

lgpdCategories is the LGPD category union: IDENTIFICATION, CONTACT, LOCATION, FINANCIAL, HEALTH, COMPANY, CREDENTIALS. check() and getEvent() fill dlpFindings, lgpdCategories, quotaExceeded, and sandbox when an older body omits them.

guard() follows finalDecision. It uses decision only when a body omits finalDecision. ALLOW runs the function. BLOCK throws VulnifyBlockedError. REVIEW throws too, unless you pass wait.

An idempotent replay of an already approved review has decision: 'REVIEW' and finalDecision: 'ALLOW'. guard() runs the function on that response and does not poll. finalDecision: 'BLOCK' throws and does not poll.

await vulnify.guard(
{
agent: 'SalesBot',
action: 'EXPORT_DATA',
resource: 'Customer Database',
destination: 'EXTERNAL_EMAIL',
recordsAffected: 12000,
},
() => exportCustomerRecords(),
);
await vulnify.guard(action, () => exportCustomerRecords(), {
timeoutMs: 120_000,
pollMs: 2000,
});

With wait, a pending review (finalDecision is REVIEW) polls waitForReview. That method returns review.status once it leaves PENDING: APPROVED, DENIED, or EXPIRED. It returns TIMEOUT when the client gives up. The function runs only when the status is APPROVED. The stored decision stays REVIEW.

const later = await vulnify.getEvent(decision.id!);
const outcome = later.finalDecision;
const status = await vulnify.waitForReview(decision.id!, {
timeoutMs: 5 * 60_000,
pollMs: 2000,
});
// status is 'APPROVED' | 'DENIED' | 'EXPIRED' | 'TIMEOUT'

getEvent() returns the same decision body as check(), including finalDecision, quotaExceeded, sandbox, and lgpdCategories. Read finalDecision on that fresh response after a review is resolved. decision remains REVIEW. There is no second webhook for the resolution. See Reviews.

getEvent throws if the response is not OK, including 429 and 5xx. It does not retry and it does not apply failMode. waitForReview uses getEvent, so a failed poll throws rather than returning a degraded decision.

VulnifyBlockedError has result (the decision). The message is Vulnify ${finalDecision ?? decision}: …, so a denial whose finalDecision is BLOCK reads Vulnify BLOCK: …. A pending review reads Vulnify REVIEW: …. When wait ends in DENIED, EXPIRED, or TIMEOUT, the error appends Review denied, Review expired, or Review timeout and still uses the decision object from the original check().

Configuration and client errors throw a generic Error, for example Vulnify request rejected (401): "Invalid API key" or a 413 when the body is over the API limit. failMode: 'open' does not catch those errors.

verifyWebhook(secret, rawBody, headersOrSignature) checks X-Vulnify-Signature, parses the body, and returns a payload discriminated by type. It throws WebhookVerificationError when the signature or body is rejected. Pass the request headers and it checks X-Vulnify-Event against type and X-Vulnify-Delivery against id. verifyWebhookSignature is the same MAC check as a boolean. vulnify 0.3.0 has its own helpers. Standard-library checks are on Webhooks.

@vulnify/sdk 0.3.0 includes the vulnify command. Run it with npx -p @vulnify/sdk vulnify. It validates policy files, pulls and applies organization policies, and runs policy tests. Install, credentials, exit codes, and a GitHub Actions example are on CLI and policies as code.

The same package exports framework adapters that do not depend on those frameworks. They call guard(), so they follow finalDecision the same way. See Framework adapters.