npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

arkveil

v0.3.0

Published

Runtime-agnostic core SDK for Arkveil — a lightweight, comprehensive ABAC platform for fine-grained access control.

Readme

arkveil

Runtime-agnostic core SDK for Arkveil — a lightweight, comprehensive ABAC platform that brings fine-grained access control to your applications through simple, structured permission formulas.

This is the framework-agnostic core. For framework integrations see @arkveil/node (Express / Fastify middleware) and @arkveil/nest (NestJS decorators and guards).

Installation

npm install arkveil
# or
yarn add arkveil
# or
bun add arkveil

Usage

import { Arkveil } from "arkveil";

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

const result = await arkveil.checkPermission({
  actionCode: "content-service.article-delete",
  user: { id: "user-123", role: "admin" },
  context: { region: "EU" },
});

if (result.granted) {
  // Allow access
} else {
  // Deny access
}

Typed Codes & Attributes

The Arkveil CLI generates one TypeScript file that types the SDK against your project — permission codes and user/context attributes — so every place that takes a code and the attribute objects get autocomplete and reject unknown or mistyped values at compile time.

arkveil generate typescript -o src/arkveil.generated.ts
// arkveil.generated.ts (auto-generated — do not edit by hand)
declare module "arkveil" {
  interface ArkveilCodeRegistry {
    codes: "content-service.article-delete" | "user-service.user-create";
  }
  interface ArkveilUserRegistry {
    attributes: { id?: string; role: "admin" | "editor" | "viewer" };
  }
  interface ArkveilContextRegistry {
    attributes: { ipAddress?: string; region?: "EU" | "US" };
  }
}

Import the generated file once for its side effect and the default generics pick everything up:

import { Arkveil } from "arkveil";
import "./arkveil.generated";

await arkveil.checkPermission({
  actionCode: "content-service.article-delete", // ✅ autocompletes
  user: { role: "admin" }, // ✅ rejects unknown keys
  context: { region: "EU" },
});

Prefer explicit generics? Pass the generated types instead of importing the file:

import type {
  ArkveilCodes,
  ArkveilUserAttributes,
  ArkveilContextAttributes,
} from "./arkveil.generated";

const arkveil = new Arkveil<
  ArkveilCodes,
  ArkveilUserAttributes,
  ArkveilContextAttributes
>({ serviceUrl, apiKey });

Until the registry is augmented, codes stay string and user / context stay Record<string, any>, so untyped usage keeps working.

API

new Arkveil(options)

| Option | Type | Default | Description | | ---------------------- | ----------------------------- | ------- | ----------------------------------------- | | serviceUrl | string (required) | — | Arkveil API service URL | | apiKey | string (required) | — | Your API key | | version | "v1" | "v1" | API version | | timeout | number | 5000 | Per-request timeout in milliseconds | | retryAttempts | number | 3 | Attempts for failed / transient requests | | getUserAttributes | (req) => user | — | Extract user attributes from a request | | getContextAttributes | (req) => context | — | Extract context attributes from a request | | logger | Logger | — | Custom logger instance | | onDenied | (req, res, reason?) => void | — | Custom handler for denied access |

checkPermission(request)

Checks a permission and resolves to { granted: boolean }. Network failures, timeouts, and transient 5xx / 429 responses are retried with exponential backoff; if the check ultimately fails it resolves to { granted: false } (fail-closed).

Row-level data protection

Arkveil can also protect data (datasets = database tables). The SDK's job is to obtain SQL enforcement artifacts from Arkveil and apply them to your application's own queries — it never receives policies, only rendered SQL (PostgreSQL-flavored today). All three endpoints share the base URL and API-key auth with checkPermission, and are served identically by the Arkveil kernel and a self-hosted arkveil-runtime sidecar — point serviceUrl at either.

A dataset code is exactly three dot-separated lowercase segments: datasource.schema.table. The SDK normalizes (trim + lowercase) before sending and throws on any other shape — there is no 2-segment shorthand and no 4-segment form.

Data policies come in three types, and each produces a different artifact:

| Type | Governs | Judged | SDK method | | -------- | ---------------------------------------- | ------------------------------------- | ------------------------------------------ | | READ | which rows a user sees | at read time | buildReadCondition | | TOUCH | which existing rows a mutation may touch | before the mutation, on current state | buildWriteChecks / buildTouchCondition | | RESULT | the state a mutation may leave rows in | after the mutation, same transaction | buildWriteChecks |

TOUCH governs UPDATE/DELETE, RESULT governs CREATE/UPDATE. Each operation has its own union of grants; a mutation whose union has no applicable policy is denied whole.

buildReadCondition(request) — filtering reads

const { readCondition } = await arkveil.buildReadCondition({
  datasetCode: "billing.public.payments",
  user: { id: "user-123", role: "manager" },
  context: {},
  alias: "p", // pass whenever the protected table is aliased or joined
});

// AND it into your query's WHERE clause:
const rows = await db.query(
  `SELECT * FROM payments p WHERE p.tenant_id = $1 AND (${readCondition})`,
  [tenantId],
);

readCondition is one SQL boolean expression. With alias omitted, columns are qualified "schema"."table"."column". FALSE is a normal response ("no applicable policy ⇒ no rows") — apply it like any other condition; never fall back to unfiltered access.

buildWriteChecks(request) — mutations over named rows

When you know the primary keys the mutation targets, ask about those rows:

const { touchSql, resultSql } = await arkveil.buildWriteChecks({
  datasetCode: "billing.public.payments",
  user: { id: "user-123", role: "manager" },
  context: {},
  operation: "UPDATE", // "CREATE" | "UPDATE" | "DELETE"
  ids: [42, 7], // required non-empty for UPDATE/DELETE; absent for CREATE
});

// Inside the mutation's transaction:
const [{ allowed: mayTouch }] = await tx.query(`${touchSql} AS allowed`);
if (!mayTouch) throw rollback();

await tx.query(`UPDATE payments SET amount = $1 WHERE id = ANY($2)`, [10, ids]);

const [{ allowed: mayResult }] = await tx.query(`${resultSql} AS allowed`);
if (!mayResult) throw rollback();

Each check is a single statement returning one boolean. Which check exists, and when it runs, is the contract:

| Mutation | touchSql (pre-state) | resultSql (post-state) | | -------- | --------------------------------- | ------------------------------------------- | | CREATE | absent | after the insert, over the inserted ids | | UPDATE | before the update, over ids | after the update, over the same ids | | DELETE | before the delete, over ids | absent |

Both run against your database, inside the mutation's transaction; false from either denies and rolls back the whole mutation. No partial success, no silent narrowing on named rows.

ids are sent as strings and inlined server-side as typed literals — so UPDATE/DELETE checks arrive ready to execute. An empty ids list targets nothing: the SDK skips the request and returns { mode: "NO_OP" } — run no checks and no mutation.

The CREATE path

A CREATE sends no ids (they exist only after the insert), so its resultSql comes back with an {{ids}} template. Insert first, then fill the template with the ids the insert produced:

import { resolveCreateResultSql } from "arkveil";

const checks = await arkveil.buildWriteChecks({
  datasetCode: "billing.public.payments",
  user,
  context: {},
  operation: "CREATE",
});

const inserted = await tx.query(
  `INSERT INTO payments (amount) VALUES ($1) RETURNING id`,
  [amount],
);
const sql = resolveCreateResultSql(
  checks,
  inserted.rows.map((r) => r.id),
);
const [{ allowed }] = await tx.query(`${sql} AS allowed`);
if (!allowed) throw rollback();

resolveCreateResultSql returns SELECT FALSE whenever the response cannot be completed (no resultSql, or one with no template), so a degraded response can never produce a check that passes. substituteIds is the lower-level helper it uses; the {{ids}} template exists on this path only.

buildTouchCondition(request) — bulk mutations by predicate

When the rows are named by a predicate rather than by id, compose the touch condition into the statement's WHERE clause instead:

const { touchCondition } = await arkveil.buildTouchCondition({
  datasetCode: "billing.public.payments",
  user,
  context: {},
  alias: "p",
  operation: "UPDATE", // "UPDATE" | "DELETE" — CREATE has no WHERE clause
});

Two recipes, each inside one transaction:

DELETE — complete as-is; a delete has no result phase:

DELETE FROM payments p WHERE p.status = 'draft' AND (<touchCondition>)

UPDATE — compose, return the ids you touched, then run only the post-state check for them:

const updated = await tx.query(
  `UPDATE payments p SET amount = $1
     WHERE p.status = 'draft' AND (${touchCondition})
     RETURNING p.id`,
  [amount],
);
const { resultSql } = await arkveil.buildWriteChecks({
  datasetCode: "billing.public.payments",
  user,
  context: {},
  operation: "UPDATE",
  ids: updated.rows.map((r) => r.id),
});
const [{ allowed }] = await tx.query(`${resultSql} AS allowed`);
if (!allowed) throw rollback();

Never run touchSql post-hoc here: the pre-state it judges no longer exists, so it would deny legitimate updates. Rows outside the subject's touch union are simply not touched — that narrowing is deliberate and visible in the query, the same trust tier as read filtration. An empty touch union renders FALSE, so the statement affects zero rows; that is the intended fail-closed composition, not an error. When the ids are known upfront, prefer buildWriteChecks — deny-whole over the enumerated rows is the stricter promise.

Fail-closed behavior

  • A well-formed dataset code that isn't registered in Arkveil comes back with SELECT FALSE in every field the operation has and reason: "METADATA_MISSING" — a configuration gap, not a policy deny. The SDK logs it distinctly; compare against the exported METADATA_MISSING constant to surface it.
  • A response missing a field its operation requires (an UPDATE with no touchSql, say), or carrying an {{ids}} template where the ids should have been inlined, is a contract violation: the SDK denies rather than proceeding unchecked and reports reason: "CONTRACT_VIOLATION". Absent fields mean "this operation has no such phase" only where the timing table above says so.
  • Transport failures and non-OK responses never widen access: after retries, buildReadCondition and buildTouchCondition resolve to "FALSE" and buildWriteChecks to SELECT FALSE in every field the operation has, all with mode: "UNAVAILABLE".
  • A malformed datasetCode, an unknown operation, or ids that contradict the operation (sent with CREATE, missing for UPDATE/DELETE) throw — those are programming errors, and the server answers them 400. No write proceeds either way: the caller never receives a check to pass.
  • mode is "NORMAL" unless the serving side is degraded (e.g. a sidecar past its staleness bound). Any other value is logged as a warning — honor the SQL, watch the diagnostics.

Field-level masking (PROJECTION policies) has no HTTP contract yet and is not part of this SDK.

Features

  • 🌍 Runtime agnostic — works in any JavaScript environment with fetch
  • 🔧 Flexible — build your own platform-specific implementations
  • 📦 Lightweight — zero runtime dependencies
  • 🔄 Retry logic — built-in retry with exponential backoff and jitter
  • 🧬 Typed codes & attributes — typed from your project's schemas

Requirements

Node.js >= 18 (uses the global fetch / AbortController).

License

MIT