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

@vistal/core

v0.3.0

Published

ORM + Access Control Layer for AI Agents

Readme

@vistal/core

The authorization layer for AI agents — zero-dependency core.

npm license TypeScript

Reads an ORM schema, generates typed LLM tools, and enforces row-level security and field-level access control server-side on every query — in code, not prompts. Adapter-agnostic: works with any ORM or database through a two-method interface.

Most users should install @vistal/prisma (Prisma / PostgreSQL / MySQL / SQLite) or @vistal/clickhouse (ClickHouse), which wrap this package with a ready-made adapter and schema introspection. Use @vistal/core directly only if you're building a custom adapter.


Installation

npm install @vistal/core

What this package exports

| Export | Purpose | |---|---| | Vistal | Main class — instantiate with an adapter, register policies, get tools | | formats.anthropic / openai / gemini | Tool formatters — convert provider-neutral tools to provider-specific shapes | | PolicyViolationError, ValidationError | Error types thrown by the policy engine | | serializeResult | Serializes Decimal, Date, BigInt in query results | | buildResultSchema | JSON Schema for the result shape of a ResolvedQuery | | Types: VistalAdapter, SchemaMap, ResolvedQuery, FilterNode, PolicyFn, PolicyResult, View, ViewResult, … | All types needed to build a custom adapter |


Building a custom adapter

An adapter is two methods: introspect() returns a SchemaMap describing your resources; execute() runs a ResolvedQuery against your database.

import type { VistalAdapter, SchemaMap, ResolvedQuery } from "@vistal/core"

class MyAdapter implements VistalAdapter {
  async introspect(): Promise<SchemaMap> {
    return {
      resources: {
        order: {
          name: "order",
          tableName: "Order",
          fields: {
            id:        { name: "id",        type: "uuid",   isId: true,  isNullable: false },
            tenant_id: { name: "tenant_id", type: "string", isId: false, isNullable: false },
            total:     { name: "total",     type: "number", isId: false, isNullable: false },
            status:    { name: "status",    type: "enum",   isId: false, isNullable: false, enumValues: ["pending", "shipped", "delivered"] },
          },
          relations: {},
        },
      },
    }
  }

  async execute(query: ResolvedQuery): Promise<unknown> {
    // query.resource  — resource name, e.g. "order"
    // query.operation — "findMany" | "findOne" | "create" | "update" | "delete" | "aggregate"
    // query.filters   — row filters AND-ed from the policy + the model's arguments
    // query.data      — write payload (create/update) with forced fields injected
    // query.include   — relation names to eager-load
    // query.sort / query.limit / query.offset
    // query.aggregations / query.groupBy
    // ... translate this into your ORM/DB call
  }
}

Then pass your adapter to Vistal:

import { Vistal } from "@vistal/core"

const vistal = new Vistal({
  adapter: new MyAdapter(),
  defaultPolicy: "deny-all",
})

Adapters may also implement an optional third method to power live views with native change notifications instead of polling:

class MyAdapter implements VistalAdapter {
  // ...
  subscribe(query: ResolvedQuery, onChange: () => void): () => void {
    // watch the underlying table(s); call onChange() when data may have changed.
    // The view then re-executes through the policy pipeline and diffs — the
    // notification never carries data, so it can never bypass policy.
    // Return an unsubscribe function.
  }
}

Policies

Register policies per resource. Each policy is a function that receives a context object and returns what is allowed:

vistal.policy("order", (ctx) => ({
  read:   { tenant_id: ctx.tenant.id },   // row filter — AND-ed into every read
  write:  { tenant_id: ctx.tenant.id },   // force-injected on INSERT, AND-ed on UPDATE WHERE
  delete: false,                           // delete_order tool never generated
  fields:    { deny: ctx.user.role === "support" ? ["internal_notes"] : [] },
  relations: { items: true, customer: ctx.user.role === "admin" },
}))

// "*" is a wildcard fallback for resources without an explicit policy()
vistal.policy("*", (ctx) => ({
  read:   { tenant_id: ctx.tenant.id },
  write:  false,
  delete: false,
}))

read, write, and delete accept:

| Value | Meaning | |---|---| | true | allow | | false | deny — no tool generated for this operation | | { field: value } | row filter (read/delete) or force-injected field (write) |


Generated tools

For each resource, vistal generates up to six tools depending on policy:

| Tool | Operation | |---|---| | query_{resource} | findMany with filters, sort, pagination, relation includes | | get_{resource} | findOne by id | | create_{resource} | insert one row | | update_{resource} | update by id | | delete_{resource} | delete by id | | aggregate_{resource} | count / sum / avg / min / max with optional groupBy |

delete: false → no delete_ tool generated. A required write field that is denied and not force-injected → create_ suppressed entirely.


Getting tools for your LLM provider

// Vercel AI SDK (requires `ai` peer dep)
const tools = await vistal.tools.vercel(ctx)
await generateText({ model, tools, maxSteps: 5, prompt })

// Anthropic
const tools = await vistal.tools.anthropic(ctx)
// tools[i].definition → pass to the API
// tools[i].execute(args) → dispatch on tool call

// OpenAI
const tools = await vistal.tools.openai(ctx)

// Gemini
const tools = await vistal.tools.gemini(ctx)

// Custom formatter
const tools = await vistal.tools.format(ctx, (t) => ({
  id: t.name,
  schema: t.parameters,
}))

Live views

Capture any read tool call as a re-executable, subscribable handle — e.g. to drive a live chart from a query the agent built, without the LLM in the loop:

const view = await vistal.view<Order>("query_order", toolCall.args, ctx)

view.resultSchema                       // JSON Schema of { data, hasMore, nextCursor? }
const { data } = await view.execute()   // data: Order[] — policies re-evaluated per call

const sub = view.subscribe(({ data }) => chart.update(data), {
  intervalMs: 5000,   // poll interval (default 5000)
  emitInitial: true,  // emit the first result immediately (default)
  onError: (e) => log.warn(e),  // polling continues after errors
})
sub.stop()

Accepts per-resource (query_x / get_x / aggregate_x) and consolidated (query + { resource }) calls; writes and meta tools throw ValidationError at creation, as do invalid args or a denied policy. Subscriptions poll + diff (emit only on change, never overlapping); when the adapter implements the optional subscribe(query, onChange) (see above), native change notifications replace the timer. Results are serialized like tool results, and onQuery events from views carry source: "view". The TS generic is developer-asserted; resultSchema is the runtime source of truth, derived from the introspected schema and policy-allowed fields at view creation.

Scale & lifecycle. Subscribers on the same View share one polling loop (late subscribers are served from cache); errors back off exponentially and reset on success; jitter (0–1) spreads polls across a fleet; VistalConfig.maxConcurrentViewQueries (default 16) caps simultaneous view executions per instance. diffKey: "id" adds row-level changes to each emission.

Persistence & governance. view.toJSON(){ vistal: "view", v: 1, toolName, args } — no ctx, by design; rehydrate with vistal.viewFromJSON(json, ctx) under a freshly resolved context. vistal.registerView(name, { toolName, args }) / openView(name, ctx) / listViews() maintain a governed catalog of allowed live queries.

Composition. compose([viewA, viewB], (a, b) => ...) runs a pure app-authored transform over multiple views and re-emits when the output changes. deriveView(view, { groupBy, aggregations, sort?, limit? }) applies a declarative, schema-validated reshape — data-only, so the spec can safely come from an agent — and derives its own resultSchema.

Codegen. generateViewTypes(view.resultSchema, "Order") emits OrderRow / OrderResult TypeScript interfaces from the runtime schema.


Type-safe resource names

Use InferResources to derive resource names from an existing typed client (e.g. Prisma):

import { Vistal, InferResources } from "@vistal/core"
import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient()

const vistal = new Vistal<DefaultContext, InferResources<typeof prisma>>({
  adapter: myAdapter,
  defaultPolicy: "deny-all",
})

// policy() and getTools() autocomplete and type-check resource names
vistal.policy("order", ...)

Observability

new Vistal({
  adapter,
  onQuery: ({ toolName, resource, operation, durationMs, error }) => {
    logger.info({ toolName, resource, durationMs })
    if (error) logger.error({ toolName, error: error.message })
  },
})

Available adapters

| Package | Database | |---|---| | @vistal/prisma | PostgreSQL, MySQL, SQLite (via Prisma 5+) | | @vistal/clickhouse | ClickHouse |


License

MIT