TypeScript/JavaScript client for Vengtoo — works with both Vengtoo Cloud and the Vengtoo Agent.
Zero dependencies. Requires Node.js 18+ (uses native fetch).
npm install @vengtoo/sdkimport { Vengtoo } from "@vengtoo/sdk";
const vengtoo = new Vengtoo({ apiKey: "azx_..." });
const allowed = await vengtoo.check({ id: "user:123", type: "user" }, "read", {
type: "document",
id: "doc:456",
});For service-to-service auth, pass clientId + clientSecret (secret is prefixed azx_cs_). The SDK exchanges credentials at the token endpoint, caches the JWT in memory, refreshes ~60s before expiry, and retries once automatically on a 401.
const vengtoo = new Vengtoo({
clientId: "my-client-id",
clientSecret: "azx_cs_...",
});Equivalent curl for the underlying token exchange:
curl -X POST https://api.vengtoo.com/v1/oauth/token \
-d grant_type=client_credentials \
-d client_id=my-client-id \
-d client_secret=azx_cs_...Providing both apiKey and OAuth credentials is rejected at construction. A bad clientId / clientSecret surfaces as a VengtooOAuthError (distinct from VengtooError) with a message pointing you at the OAuth exchange.
const vengtoo = new Vengtoo({ baseUrl: "http://127.0.0.1:8181" });const resp = await vengtoo.evaluate({
subject: { id: "user:123", type: "user" },
action: { name: "read" },
resource: { type: "document", id: "doc:456" },
context: { ip: "10.0.0.1" },
});
// resp.decision, resp.context?.reason, resp.context?.policy_id, resp.context?.access_pathEvaluate up to 50 checks in one round-trip (AuthZEN 1.0). Top-level fields act as defaults that individual items inherit:
const resp = await vengtoo.evaluateBatch({
subject: { id: "user:123", type: "user" }, // default for all items
action: { name: "read" },
evaluations: [
{ resource: { type: "document", id: "doc:1" } },
{ resource: { type: "document", id: "doc:2" } },
],
});
// resp.evaluations[i].decision, positionalWhen a policy requires human approval, evaluate() returns
reason_code: "authorization_pending". evaluateWithApproval() handles the
wait — polling at the server-recommended interval until a human approves or
denies in the Vengtoo dashboard:
const resp = await vengtoo.evaluateWithApproval(req, {
timeoutMs: 5 * 60_000,
onPending: (authReqId, expiresIn) =>
console.log(`waiting for approval ${authReqId} (expires in ${expiresIn}s)`),
signal: abortController.signal, // optional: cancel the wait
});
// Terminal reason_codes: "approved_by_human", "access_denied",
// "approval_timeout" (no human answered), "polling_error" (network),
// "canceled" (your signal aborted). Never throws for these — always fails closed.Grant an agent the delegator's permission scope for exactly the duration of a task — created before, revoked after, even when the task throws. A failed revocation is reported in the thrown error, never swallowed:
const result = await vengtoo.withDelegation(
{
delegator_id: userEntityId,
delegate_id: agentEntityId,
scope: ["invoices:read", "invoices:submit"], // optional: attenuate further
description: "Q3 invoice processing run", // optional: shows in audit/dashboard
},
async (delegationId) => runWorkflow(),
);The delegate's effective permissions are always the intersection of its own
policies and the delegator's — scope narrows that further, it can never
escalate.
Two layers, two jobs: the middleware is the route-level perimeter ("may
this caller touch this API area at all?") — it evaluates type-level policies
and catches routes you forgot to protect. For per-object decisions, call
check()/evaluate() inside the handler where the resource is known.
The middleware takes a subject extractor — you tell it where your authentication layer put the caller's identity (mount authn before this):
app.use(
"/documents",
vengtoo.middleware("document", "read", (req) => {
const user = (req as any).user; // set by your authn middleware
if (!user) throw new Error("unauthenticated");
return { id: user.id, type: "user" };
}),
);Or from a trusted gateway header — only safe when an edge component you control authenticates callers and sets the header; if clients can reach this service directly, they can impersonate anyone:
vengtoo.middleware("document", "read", (req) => {
const id = req.headers["x-user-id"]; // set by our API gateway
if (!id || Array.isArray(id)) throw new Error("missing identity header");
return { id, type: "user" };
});An extractor throw (or a subject with no id/external_id) → 401. Policy
deny → 403. Authorization infrastructure failure → 500, fail closed.
new Vengtoo({
apiKey: "azx_...", // API key for cloud mode
baseUrl: "http://127.0.0.1:8181", // Custom URL (agent mode)
timeout: 5000, // Per-request timeout in ms (default: 10000)
maxRetries: 3, // Max retries on 5xx/429 (default: 2)
});import { VengtooError, VengtooOAuthError } from "@vengtoo/sdk";
try {
await vengtoo.evaluate(req);
} catch (err) {
if (err instanceof VengtooOAuthError) {
/* bad clientId/clientSecret */
}
if (err instanceof VengtooError) {
err.isAuthError; // 401 — bad API key or expired token
err.isForbidden; // 403
err.isNotFound; // 404
err.isServerError; // 5xx — retries exhausted
}
}The SDK automatically retries on 5xx and 429 responses (default: 2 retries,
honoring the server's Retry-After hint). Other 4xx errors are never retried.
With OAuth, a 401 triggers one token refresh before failing.
interface Subject {
type: string; // required
id?: string; // id or external_id required
external_id?: string;
properties?: Record<string, unknown>;
}
interface Resource {
type: string; // required
id?: string; // omit both id and external_id for type-level checks
external_id?: string;
properties?: Record<string, unknown>;
}
interface Action {
name: string; // required
properties?: Record<string, unknown>;
}
interface EvaluationRequest {
subject: Subject;
resource: Resource;
action: Action;
context?: Record<string, unknown>;
}
interface EvaluationResponse {
decision: boolean;
context?: EvaluationContext; // reason, reason_code, policy_id, access_path + HITL fields
}Required on every check (matching the API and AuthZEN 1.0): subject.type,
subject.id (or external_id), resource.type, and action.name. The SDK
validates these locally so you get an immediate, clear error instead of a
server 400.
Apache-2.0 — see LICENSE.