Arkveil
SDK

Core SDK (arkveil)

Runtime-agnostic core SDK — the Arkveil client, permission checks, and row-level data condition builders.

Install

npm install arkveil

arkveil has zero runtime dependencies and ships dual ESM/CJS builds with type declarations.

Creating a client

import { Arkveil } from "arkveil";

const arkveil = new Arkveil({
  serviceUrl: "https://api.arkveil.com",
  apiKey: "your-api-key",
});

Constructor options

OptionTypeDefaultDescription
serviceUrlstringRequired. Base URL of Arkveil Cloud or a self-hosted arkveil-runtime sidecar.
apiKeystringRequired. Workspace API key, sent as the x-api-key header.
version"v1""v1"API version.
timeoutnumber5000Request timeout in milliseconds.
retryAttemptsnumber3Retry attempts for 429/5xx/network errors.
loggerLoggerLogger with error/warn methods, used for fail-closed diagnostics.
getUserAttributes(req: any) => TUser | Promise<TUser>Extracts user attributes from a framework request object. Used by platform SDKs.
getContextAttributes(req: any) => TContext | Promise<TContext>Extracts context attributes from a framework request object.
onDenied(req, res, reason?) => void | Promise<void>Custom handler invoked on denial by platform SDKs.

Arkveil is generic over <TCode, TUser, TContext>. Leave these unset to fall back to string/Record<string, any>, or bind them to generated types — see Typed codes & attributes.

Permission checks

const result = await arkveil.checkPermission({
  actionCode: "content-service.article-delete",
  user: { id: "user-123", role: "admin" },
  context: { region: "EU" },
});
// result: { granted: boolean }
  • checkPermission(request) POSTs to ${serviceUrl}/api/${version}/abac/permissions/check.
  • It is fail-closed: any network error, timeout, or non-OK response is caught, logged, and resolved to { granted: false }. It never throws.
  • buildPermissionRequest(code, req) resolves user/context attributes from a framework request via getUserAttributes/getContextAttributes, returning a PermissionCheckRequest. This is used internally by @arkveil/node and @arkveil/nest.

Row-level data protection

Two helpers turn an ABAC policy into SQL you execute directly against your own database. The SDK never sees your data — only the rendered condition or check SQL.

buildReadCondition

const { readCondition, mode } = await arkveil.buildReadCondition({
  datasetCode: "billing.public.payments",
  user: { id: "user-123", role: "manager" },
  context: {},
  alias: "p",
});

const rows = await db.query(
  `SELECT * FROM payments p WHERE p.tenant_id = $1 AND (${readCondition})`,
  [tenantId],
);
  • POSTs to .../abac/conditions/read.
  • datasetCode is the canonical dataset code, datasource.schema.table (see dataset codes).
  • Pass alias when the table is aliased or joined, so the condition qualifies columns as "alias"."column" instead of "schema"."table"."column".
  • readCondition: "FALSE" is a normal response meaning "no policy grants access to any rows" — not an error.
  • Fails closed to { readCondition: "FALSE", mode: "UNAVAILABLE" } on any error.

buildWriteChecks

const { writeSql, invariantSql } = await arkveil.buildWriteChecks({
  datasetCode: "billing.public.payments",
  user,
  context: {},
  ids: [42, 7],
});

const [{ allowed }] = await tx.query(`${writeSql} AS allowed`);
if (!allowed) throw rollback();
  • POSTs to .../abac/conditions/write.
  • writeSql is a single statement returning a boolean; run it inside the same transaction as the mutation:
    • CREATE — after insert, with the new row ids.
    • UPDATE — before, with the targeted ids.
    • DELETE — before, with the targeted ids.
  • ids are sent as strings; the server casts them to the dataset's primary key type. Omitting ids leaves a {{ids}} placeholder in writeSql, which you can fill later with substituteIds.
  • reason: "METADATA_MISSING" means the dataset isn't registered in Arkveil — a configuration gap, not a policy denial.
  • Fails closed to { writeSql: "SELECT FALSE", invariantSql: [], mode: "UNAVAILABLE" } on any error.

Dataset codes

Dataset codes identify a table for row-level protection and follow a strict shape: datasource.schema.table, three lowercase, dot-separated identifier segments.

import { normalizeDatasetCode } from "arkveil";

normalizeDatasetCode("Billing.Public.Payments");
// "billing.public.payments"

normalizeDatasetCode trims and lowercases a dataset code, and throws if it isn't exactly three dot-separated identifier segments. This is the one place the SDK throws instead of failing closed, because a malformed code is a configuration bug, not a runtime access decision.

Helper exports

import {
  normalizeDatasetCode,
  substituteIds,
  IDS_PLACEHOLDER,
  METADATA_MISSING,
  MODE_UNAVAILABLE,
} from "arkveil";
ExportDescription
normalizeDatasetCode(code: string): stringValidates and normalizes a dataset code. Throws on malformed input.
substituteIds(writeSql: string, ids: readonly (string | number | bigint)[]): stringReplaces the {{ids}} placeholder in writeSql with escaped SQL literals. Throws if ids is empty or no placeholder is present.
IDS_PLACEHOLDER"{{ids}}"
METADATA_MISSING"METADATA_MISSING"
MODE_UNAVAILABLE"UNAVAILABLE"

Types

import type {
  ArkveilParams,
  ArkveilCode,
  ArkveilCodeRegistry,
  ArkveilUser,
  ArkveilUserRegistry,
  ArkveilContext,
  ArkveilContextRegistry,
  PermissionCheckRequest,
  PermissionCheckResponse,
  ReadConditionRequest,
  ReadConditionResponse,
  WriteChecksRequest,
  WriteChecksResponse,
  WriteCheckId,
} from "arkveil";

The *Registry interfaces are empty by default and are augmented by generated code — see Typed codes & attributes.

On this page