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

@priventai/core

v0.16.0

Published

AI agent runtime security: context-preserving tokenization, risk scoring, and policy enforcement

Downloads

749

Readme

@priventai/core

Early Access. Privent is currently in private rollout. API keys are issued through our access process. Request access →

Runtime security for AI agents. Tokenize PII, secrets, and sensitive data before they reach LLMs; restore the originals only at trusted egress points.

"Customer: [email protected], Card: 4111-1111-1111-1111"
                          ↓ tokenize
"Customer: [EMAIL_001], Card: [CREDIT_CARD_001]"
                          ↓ LLM call
"Reply to [EMAIL_001] about [CREDIT_CARD_001]"
                          ↓ detokenize at trusted sink
"Reply to [email protected] about 4111-1111-1111-1111"

The LLM never sees raw secrets. Tokens are reversible, session-scoped, and deterministic within a session.


Features

  • Hybrid detection — regex patterns for EMAIL, PHONE, CREDIT_CARD, IBAN, SSN, API_KEY, JWT, AWS_KEY, IP, URL out of the box; pluggable ML extractor.
  • Session-scoped vault — same value maps to the same token within a session; cross-session tokens are randomized to block correlation attacks.
  • Deep detokenization — walks nested objects, arrays, and strings; cycle-safe.
  • Typed error taxonomyPriventError base class plus 9 subclasses, each with a retryable flag and JSON serialization that scrubs sensitive fields.
  • Resilient HTTP client — exponential backoff with jitter, idempotency keys, AbortController-based timeouts.
  • Fail-open by default — if Privent Cloud is unreachable, falls back to local regex detection rather than blocking traffic.
  • Dual ESM + CJS build with full TypeScript types.

Installation

npm install @priventai/core
# or
pnpm add @priventai/core
# or
yarn add @priventai/core

Requires Node.js 20+.


Quick Start

import { PriventClient } from '@priventai/core';

const client = new PriventClient({
  apiKey: process.env.PRIVENT_API_KEY, // request via https://www.privent.ai/request-access — without it, runs in regex-only mode
});

await client.withSession(async ({ vault }) => {
  const { tokenizedText } = await client.tokenizer.tokenize(
    'Customer: [email protected], Card: 4111111111111111',
    vault,
    { kinds: ['EMAIL', 'CREDIT_CARD'] },
  );
  // tokenizedText === "Customer: [EMAIL_001], Card: [CREDIT_CARD_001]"

  const llmResponse = await myLLM.invoke(tokenizedText);

  const restored = await client.tokenizer.detokenize(llmResponse, vault);
  return restored;
});
// vault.destroy() runs automatically in a finally block

API

PriventClient

Main entry point. Wires together a vault, tokenizer, risk scorer, policy engine, and audit logger.

const client = new PriventClient({
  apiKey?: string;                 // defaults to PRIVENT_API_KEY env var
  baseUrl?: string;                // default: https://api.privent.ai
  vaultFactory?: VaultFactory;     // default: in-memory
  tokenizer?: Tokenizer;           // default: HybridTokenizer (regex)
  riskScorer?: RiskScorer;
  policyEngine?: PolicyEngine;
  auditLogger?: AuditLogger;
  maxRetries?: number;             // default: 2
  timeout?: number;                // default: 30_000 ms
  failPolicy?: 'open' | 'closed';  // default: 'open'
});

client.withSession(fn)

Opens a session, gives fn a vault and IDs, and destroys the vault when fn resolves or throws.

const result = await client.withSession(async ({ vault, sessionId, traceId }) => {
  // vault: TokenVault — store/retrieve tokens
  // sessionId: per-session UUID
  // traceId: correlation ID for audit logs
  return doWork(vault);
});

Tokenizer

const { tokenizedText, entities } = await client.tokenizer.tokenize(text, vault, {
  kinds: ['EMAIL', 'PHONE'],
  allowList: ['[email protected]'],   // never tokenize
  denyList: ['internal-project-x'],    // always tokenize
  customPatterns: [
    { kind: 'PROJECT_CODE', regex: /PROJ-\d{4}/g, confidence: 0.95 },
  ],
});

Token format: [KIND_NNN] — e.g. [EMAIL_001], [CREDIT_CARD_003]

Built-in entity types:

| Kind | Confidence | Notes | |---|---|---| | EMAIL | 0.95 | RFC-pragmatic match | | PHONE | 0.80 | International + national formats | | CREDIT_CARD | 0.98 | Validated with Luhn checksum | | IBAN | 0.97 | Country-aware length check | | SSN | 0.90 | US format | | API_KEY | 0.88 | Common provider prefixes (sk-, ghp_, xoxb-, …) | | JWT | 0.98 | Three-segment base64url structure | | AWS_KEY | 0.99 | AKIA… access key IDs | | IP_ADDRESS | 0.85 | IPv4 | | URL | 0.90 | http/https |

detokenizeDeep(value, vault)

Walks any value (string, object, array, Map, Set) and replaces tokens with their originals.

import { detokenizeDeep } from '@priventai/core';

const restored = await detokenizeDeep(
  { email: '[EMAIL_001]', items: ['[PHONE_001]', 'plain text'] },
  vault,
);

Cycle-safe (uses WeakSet), depth-limited to 64, skips binary buffers, fast-paths strings without [.

Vault

import { InMemoryTokenVault } from '@priventai/core/vault/memory';

const vault = new InMemoryTokenVault('session-1');
await vault.store({ token: '[EMAIL_001]', value: '[email protected]', kind: 'EMAIL', ... });
const entry = await vault.retrieve('[EMAIL_001]');
const same = await vault.findByValue('[email protected]', 'EMAIL'); // determinism
await vault.destroy();

Normalization for determinism: EMAIL → lowercase + trim; PHONE, CREDIT_CARD, IBAN → digits only; others → trim.

Errors

import {
  PriventError,
  PriventConfigError,
  PriventAuthError,
  PriventRateLimitError,
  PriventAPIError,
  PriventNetworkError,
  PriventTimeoutError,
  PriventValidationError,
  PriventVaultFullError,
  PriventVaultDestroyedError,
} from '@priventai/core';

try {
  await client.tokenizer.tokenize(text, vault);
} catch (err) {
  if (err instanceof PriventError) {
    console.log(err.code, err.retryable, err.toJSON());
  }
}

toJSON() scrubs sensitive fields so errors are safe to log.


Audit Events

The AuditEvent.type union covers eight kinds: session_open | tokenize | detokenize | risk_check | llm_call | policy_decision | egress | error. 'llm_call' is emitted by the external @priventai/n8n-hook package; the in-tree n8n-nodes-privent package emits the first four (plus error on failure paths).

Wire contract: every event is serialized to the v1 schema (POST /v1/audit/events) at flush time. The TypeScript AuditEvent interface is camelCase (traceId, sessionId, workflowId, nodeId, numeric timestamp, optional framework: 'manual'); the wire payload is snake_case (trace_id, session_id, workflow_id, node_id, ISO8601 timestamp). A UUID event_id is generated per event for backend idempotency, and framework: 'manual' is silently coerced to 'sdk' on the wire (the value is deprecated).

AuditEvent.metadata is a free-form Record<string, unknown> on the wire — adapters populate snake_case keys (agent_name, workflow_name, execution_id, node_name) using the NodeAuditContext typing aid, plus event-type-specific extras (sink_id / sink_url_host / sink_trusted on detokenize, risk_score / risk_level / categories on risk_check, prompt_tokens / completion_tokens / latency_ms / provider / model on llm_call, etc.).

The v1 wire schema also accepts optional top-level scalars and config blocks that the backend denormalizes into indexed columns and auto-discovered resource tables, so dashboards can read them without JSON-extract at query time:

| Top-level field | Purpose | |---|---| | latency_ms | Per-event latency (int, ms). Surfaced as the node's P50/P95/P99 in the inspector. | | error_type | Short error classifier (≤64 chars) used for the error-breakdown panel. | | node_name | Mirror of metadata.node_name; promoted for indexed filtering. | | http_status | Response code for sink/egress events; powers the 4xx/5xx KPI cells. | | webhook_config | { url, method?, auth_scheme?, content_type? } — backend upserts a Webhook row by (org, url, method). | | sink_config | { sink_key, host, trusted?, tls_version?, cert_fingerprint?, added_by_user_id? } — upserts a Sink row by (org, sink_key). | | vault_config | { name, region?, retention_days?, detection_mode?, detection_version? } — upserts a Vault row by (org, name). | | cron_config | { cron_expression, timezone? } — upserts a CronSchedule row by (org, cron_expression, workflow_id). |

All seven fields are optional. Existing callers that don't set them continue to work — the backend treats every event without the new fields exactly as it did before. The AuditEvent interface exposes the same fields in camelCase (latencyMs, errorType, nodeName, httpStatus, webhookConfig, sinkConfig, vaultConfig, cronConfig); serializeForWire handles the snake_case translation.

Critical events flush immediately rather than waiting for the buffer interval: any error event, and policy_decision events whose metadata.decision === 'BLOCK'.

Sample wire payload for a sink egress event using both metadata and the new top-level shape:

{
  "event_id": "8a3f…",
  "type": "egress",
  "trace_id": "…",
  "session_id": "…",
  "timestamp": "2026-05-23T12:00:00.000Z",
  "framework": "n8n",
  "workflow_id": "abc123",
  "node_id": "<node uuid>",
  "node_name": "Notify Slack",
  "latency_ms": 412,
  "http_status": 200,
  "sink_config": {
    "sink_key": "https://hooks.slack.com/services/T0/B0/x",
    "host": "hooks.slack.com",
    "trusted": true,
    "tls_version": "TLS1.3",
    "cert_fingerprint": "sha256/AAA…"
  },
  "metadata": {
    "agent_name": "support-bot",
    "workflow_name": "Inbound Triage",
    "execution_id": "exec-42",
    "node_name": "Notify Slack",
    "framework": "n8n"
  }
}

Security Properties

  • No persistence by default. The in-memory vault holds entries for the session lifetime only.
  • Cross-session randomization. Token IDs are not derived from input value — observing tokens across sessions reveals nothing.
  • Bounded vaults. Default cap of 10,000 entries per session prevents unbounded growth.
  • Sensitive fields scrubbed in error JSON. API keys, tokens, and entity values are stripped before serialization.
  • Idempotency keys on every Cloud request to make retries safe.

Documentation


License

Apache-2.0 © Privent AI

Questions? Contact us at [email protected].

Verifying this package

@priventai/core is reproducible: a clean checkout of a release tag rebuilds the published tarball's contents byte for byte. Two things will make a correct verification look like a mismatch, so both are stated here rather than left for you to discover.

Use pnpm pack, not npm pack

This is a pnpm workspace and its manifest uses the catalog: protocol. npm pack does not resolve it; the publish path does. Verify with npm pack and you will see a real content difference in package/package.json

published   "zod": "^3.23.0"      "tsup": "^8.3.0"
npm pack    "zod": "catalog:"     "tsup": "catalog:"

— and conclude the artifact does not match its source. It does. The packer was wrong.

Compare the inner tar, not the .tgz

A gzip frame carries an OS byte written by whoever packs: 0x03 from Linux, 0x13 from macOS. Two byte-identical builds therefore produce different .tgz checksums on different machines. The tar is the content-bearing unit; the frame is not.

The commands

git checkout '@priventai/core@<version>'
pnpm install --frozen-lockfile
pnpm --filter @priventai/core build

cd packages/core && pnpm pack --pack-destination /tmp/built
npm pack "@priventai/core@<version>" --pack-destination /tmp/published

# compare contents, ignoring the gzip frame
diff <(gunzip -c /tmp/built/*.tgz | shasum -a 256) \
     <(gunzip -c /tmp/published/*.tgz | shasum -a 256)

Or run the checker this repository uses, which does both comparisons and prints what each one does not answer:

node scripts/verify-artifact.mjs --against <version>

Upgrading to 0.16.0 — overlapping spans mask the union

Behavioural. Your output can differ. When two detections overlap — a custom pattern and a built-in, or an ML span and a regex match — they used to compete and one was discarded. They now produce one masked unit covering the union, and the candidate with the widest region supplies the label.

If you depend on exact token identity across overlaps, expect a wider token, fewer tokens, or a different kind. contact [email protected] today with a /jane\.doe/ pattern produced contact [PROJECT_001]@acme.com today in 0.15.1 and produces contact [EMAIL_001] today now.

Nothing is lost: every contributing candidate is on TokenizedEntity.candidates.

What it fixes, both live in 0.14.2 and 0.15.1: a custom pattern reduced masking (17 characters → 8, mail domain in the clear, nothing recorded), and an ML span was discarded for a shorter regex match — the second needs no custom pattern, so every ML-enabled deployment was affected.

No published coverage number changes. Our eval corpus contains neither custom patterns nor overlapping ML spans, so masked characters are identical before and after. This release repairs paths that corpus does not contain.

0.15.0 is uninstallable — use 0.15.1

0.15.0 published with zod: "catalog:", a pnpm workspace protocol that leaked into the registry manifest. Neither npm nor pnpm can install it. 0.15.1 is the same code, published correctly — everything in the section below applies to it unchanged.

Upgrading to 0.15.0 — a masking floor, and a type change that finds an existing bug

Read the type change first, because it is the one that will stop your build. ExtractionMetaV1.latencyMs and buildSignature are now optional. They were typed as required and the ML service has never emitted either, so the types said number and string while the runtime value was undefined. meta.latencyMs.toFixed(1) compiled and threw. Nothing about that risk is new in 0.15.0 — the compiler just stopped hiding it. The sites TypeScript now flags are the crash sites you already had.

BEHAVIOURAL, and the one to test before you ship it: less text is masked. HybridTokenizer now applies a confidence floor to what it SUBSTITUTES. DEFAULT_TRANSFORM_FLOOR is 0.6. Findings below it are left in your text and reported on the new TokenizeResult.observed, with their confidence, the floor they were measured against, and why they were not substituted. Nothing is dropped for being below the floor.

observed covers every exclusion at the ML admission gate, not only the floor — declining by source is still declining. reason is one of below-transform-floor (carries the floor), source-not-admitted (the producer's source is not one the tokenizer substitutes from — regex, hint, or none declared — carried on the record), or allow-listed (your own allowList). Each carries a detail. What it does not cover: overlap resolution runs after this gate, so a finding discarded by removeOverlaps does not appear — a different stage, unchanged here.

source-not-admitted closes a silent drop that predates this release: a regex- or hint-sourced finding no local detector covered was excluded from transformation and absent from every output field. It is still not substituted; it is now reported.

If your integration depends on the previous behaviour — every ML finding substituted regardless of confidence — pass a lower floor as the fourth argument to the HybridTokenizer constructor. An overridden value logs once at construction, on purpose: a threshold that decides what gets masked is a security control, and an operator has to be able to see its effective value.

Measured end to end over 257 annotated texts, counting characters substituted that no label calls sensitive: 78 spans / 1256 chars before, 31 spans / 913 chars after. The 47 spans / 343 chars closed are all model-sourced person names at confidence exactly 0.55 — the ML service's spaCy bridge, whose score is a fixed uncalibrated constant and which measured 0.4554 standalone precision.

Stated plainly, because the good half alone would mislead: this closes the model-source corruption completely and by construction; it does not close SDK corruption. 913 characters still get substituted, and the mechanisms that would close them are an allowList and a detector-side regex fix, both outside this release. Anyone reading only the good half would be reading a blended number.

The rejection this removes was never selective. A single unreadable field did not cost you that field — it threw out the whole ML pass, including every well-formed entity in the same response. On a response carrying one unreadable entity and one good one, 0.14.2 masks 0 of 2; 0.15.0 masks 1 of 2 and reports the other. This release is not only a floor; it is the end of all-or-nothing response validation.

An incomplete extraction now says so. When the ML service reports that it could not finish, TokenizeResult.extractionDegraded is present — so a short entity list means "the detector could not look", not "nothing was found". It is a separate field from mlDegradation on purpose: extractionDegraded.origin is 'server' (the call succeeded, the server reported a gap), while mlDegradation is this client's own pass failing. A degraded pass still masks everything it did manage to find.

Not breaking, but worth knowing: response schemas are now forward-compatible, so a field the server adds can no longer disable this client — previously ExtractionMetaSchema rejected every shape the ML service produces, and the rejection surfaced as "ML service unavailable" with a silent fall back to regex-only detection. A degraded ML pass now names its reason on TokenizeResult.mlDegradation. Request schemas are unchanged and still strict. The cloud request also declares client_capabilities, so a server can extend extraction_meta without breaking clients on 0.14.2 and earlier; if the server is too old to know the field, TokenizeResult.capabilitiesDeclined says so.

Upgrading to 0.14.0 — /v1/risk/score responses are validated

If you are already on 0.14.0, you have this behaviour and did not get this notice. 0.14.0 shipped without a changelog in its tarball and without this section in its README, so nothing on the registry page told you. The change is in 0.14.0, not in 0.14.10.14.1 adds only this documentation and changes no code.

BREAKING. A malformed /v1/risk/score response now throws instead of scoring. CloudRiskScorer.score() and scoreBatch() raise PriventValidationError, naming the field and what was expected, when the response has a declared field of the wrong JSON type or a required field missing.

Previously they returned a synthetic LOW-risk score — a risk score that was never computed. If your application branched on a LOW result, it may have been branching on a body this SDK could not parse.

What deliberately does not throw

  • Unknown or extra fields — ignored. A strict schema on a response is a promise the server will never add a field, and nobody made that promise.
  • Format mismatches on a declared field of the right type.
  • Transport failures — still fail open to model: 'fallback'.
  • ML enrichment — still degrades to regex-only detection. No score is fabricated there, so nothing is masked.

There is no flag to disable validation. If you need to continue on an unparseable body, catch PriventValidationError and decide in your own code, where the decision is visible.

What is NOT validated

Validation is opt-in per call site. These five have none, so you can check whether the endpoint you use is covered:

POST /v1/risk/batch (a schema exists but no captured response has verified it), POST /v1/vault/find-or-create-batch, POST /v1/vault/retrieve-batch, POST /v1/vault/destroy, and privent-ml's POST /classify.

Also in 0.14.0

ScoreResponseSchema now describes what the endpoint actually emits, verified against a captured response body rather than a DTO: undeclared fields are no longer rejected, model is a string with no format constraint, and risk_level accepts null.

Upgrading to 0.13.0 — riskScore.categories

riskScore.categories is a list of category names, not a map of scores. categories.pii was always undefined.

// before — compiled, returned undefined
const score = riskScore.categories.pii;

// after — compiles, and works
const hasPii = riskScore.categories.includes('pii');

Nothing changes at runtime. The value was always an array; only the type lied. If your code read categories.pii it was already getting undefined — the type system was telling you it was fine. If your code stopped compiling on this upgrade, that is the fix reaching you.

Measured against privent-backend at origin/dev 191ccc1: POST /v1/risk/score declares categories: string[], all five of its exits are array-shaped, and its own end-to-end test asserts toContain('financial') on a real HTTP response body — which would not pass on an object.

Why this is 0.13.0 and not 1.0.0

1.0.0 is a claim about the whole surface, and two findings on that surface are still open: ScoreResponseSchema is published but never enforced against a real response (SDK-Q), and the contract parity test cannot disagree with the schema it validates, because both have the same author (SDK-R). A package whose published contract is never checked against a real response is not one to call stable. 1.0.0 becomes sayable when those two close.