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

@altrsoftware/shield

v2.0.0

Published

ALTR Shield Node.js SDK — protect and restore sensitive data via the Shield data plane, with an XML token envelope and streaming detokenization.

Readme

@altrsoftware/shield

npm version license

Node.js SDK for the ALTR Shield data plane. Protect sensitive data (classify + tokenize/mask per policy) before it leaves your trust boundary, and restore it — where policy allows — on the way back. Protected values are carried in-text as an XML token envelope (<altr tok="…"/>), so prompts, memories, tool arguments, and streamed model output stay structurally intact while carrying only non-sensitive surrogates.

  • ProtectPOST /v1/protect classifies your text; the SDK splices the findings back in as token tags and mask literals, byte-offset-safe for any Unicode.
  • Restore / detokenize — swap policy-allowed tokens back for their values; denied tokens stay as tags. Restoration is identity-aware: what detokenizes depends on the calling application and the tags on both the request and the stored token.
  • Streaming — a chunk-boundary-safe TransformStream / async-iterable detokenizer for LLM output.

ESM-only, and runtime-agnostic — Node, Edge/Workers, Deno/Bun, and browsers (server-side; see Runtime support).

Install

npm install @altrsoftware/shield
# or: pnpm add @altrsoftware/shield / yarn add @altrsoftware/shield

Getting your credentials

Everything the constructor needs comes from your ALTR organization (ask your ALTR admin, or see the ALTR docs for your deployment):

| Option | What it is | Where it comes from | | ---------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | baseUrl | Shield data-plane URL, e.g. https://<your-org>.shield.altr.com | Provided when Shield is enabled for your org | | orgId | Your ALTR org id (sent as the JWT client_id claim) | ALTR organization settings | | appId | Shield application id (JWT shield_app_id claim) | Created when you register a Shield Application | | privateKeyPem | The application's registered RSA private key, PKCS#8 PEM | You generate the keypair; the public key is registered on the application (two rotation slots) | | collectionName | Classifier collection that decides what counts as sensitive | Created/managed in ALTR |

Generate a keypair:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out app-private.pem
openssl pkey -in app-private.pem -pubout -out app-public.pem
# register app-public.pem on your Shield Application

The SDK mints short-lived JWTs from the private key on demand — there is no long-lived token to manage.

Quickstart

import { readFileSync } from "node:fs";
import { ShieldClient } from "@altrsoftware/shield";

const shield = new ShieldClient({
  baseUrl: "https://<your-org>.shield.altr.com",
  orgId: "<YOUR_ALTR_ORG_ID>",
  appId: "<YOUR_SHIELD_APP_ID>",
  privateKeyPem: readFileSync("app-private.pem", "utf8"),
  collectionName: "<YOUR_COLLECTION_NAME>",
});

const { protectedText, tokens, findings } = await shield.protect(
  "Hi, I'm Jane Doe. My card is 4111111111115462.",
  { tags: ["conv:sess_123"] },
);
// protectedText → Hi, I'm <altr tok="9edc…"/>. My card is ************5462.

const { text, restored, denied } = await shield.restore(protectedText, {
  tags: ["conv:sess_123"],
});
// text → policy-allowed tokens restored; denied tokens stay as tags

Request-scope tags are stamped onto every token a protect call mints and are evaluated against restore policy — sending the same correlator tag (a conversation id, an actor id) on both protect and detokenize is what makes tag-scoped, identity-aware restore policies work.

Deterministic tokenization is scoped by an optional determinismContext (set it on the client or per protect() call): under a deterministic policy strategy, the same value in the same context always resolves to the same token, and a different context yields an unlinkable one. The context is compared byte-exact — never trimmed or case-folded ("Fruit""fruit") — and capped at 256 UTF-8 bytes (MAX_DETERMINISM_CONTEXT_BYTES); omitted and "" are the same (default) scope.

detokenize(tokens, { tags }) is the batch primitive underneath restore(). Every token you pass gets an entry in the returned values map; a token that did not resolve (denied, unknown, or missing from the response) maps to itself — test with values[token] === token, never with truthiness.

findings is discriminated on action: switch on it to get finding.token (tokenize) or finding.masked_as (mask) as plain strings. A finding this SDK version cannot represent fails the whole protect() call with ShieldSpliceError instead of reaching your code. See docs/node-sdk.md for the full API walkthrough.

Streaming LLM output

Model output can split an <altr tok="…"/> tag across any chunk boundary; the streaming detokenizer buffers only the smallest suffix that could still be a tag and swaps complete tags as they close:

import {
  createDetokenizeStream,
  detokenizeIterable,
} from "@altrsoftware/shield";

// Web Streams — decode bytes first; a response body streams Uint8Array:
if (!modelResponse.body) throw new Error("no response body");
modelResponse.body
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(createDetokenizeStream(shield, { tags: ["conv:sess_123"] }))
  .pipeTo(destination); // destination = any WritableStream (an HTTP response, a file, ...)

// Async iterable (agent loops, SSE handlers):
for await (const piece of detokenizeIterable(shield, chunks, { tags })) {
  response.write(piece);
}

// A Node stream yields Buffers unless you ask for text:
nodeReadable.setEncoding("utf8");
for await (const piece of detokenizeIterable(shield, nodeReadable, { tags })) {
  response.write(piece);
}

Both flavors take decoded text — a Buffer/Uint8Array chunk throws ShieldError, because a byte chunk can end mid-UTF-8-sequence. Decode first with setEncoding("utf8"), a TextDecoderStream, or one TextDecoder reused with { stream: true }.

Repeated tokens cost one lookup per stream (denials cached too, up to a 10,000-unique-token per-stream cache). On a mid-stream detokenize failure the default is to error the stream; errorMode: "passthrough" emits the affected tags verbatim and keeps streaming, with an optional onDetokenizeError observer so failures aren't invisible.

Tags resolve per incoming chunk, which keeps output incremental. Streaming input is usually model output an attacker can influence directly, so bound it with a signal + timeout just like restore() — see docs/security-notes.md.

Retries

Transient failures — network errors, 429, 500, 502, 503, and 504 — retry automatically with jittered exponential backoff (base 500 ms, capped at 5 s), honoring a server Retry-After. Both endpoints are safe to repeat: detokenize is a pure read, and a deterministic vault returns the identical token on a protect retry. The default budget is 2 retries — tune with maxRetries on the constructor (credential form only; a custom transport owns its own retry policy) or per call ({ maxRetries: 0 } disables). A gateway-authorizer 401/403 additionally replays exactly once with a freshly minted JWT. Detokenize batches that trip the 422 response-size cap split themselves automatically and continue.

Cancellation & timeouts

Every network-touching call accepts an AbortSignal, combined with the transport's own per-request timeout (default 30 s, configurable via timeoutMs):

const controller = new AbortController();
const pending = shield.protect(text, { signal: controller.signal });
controller.abort(); // pending rejects with the abort reason, not a ShieldNetworkError

restore() has no built-in cap on how many tokens it extracts — untrusted text carrying thousands of token-shaped tags fans out into many sequential round trips. When text is untrusted (stored chat history, an uploaded document), pass a signal wired to a timeout and bound the size of text yourself (details in docs/security-notes.md):

const controller = new AbortController();
const timer = setTimeout(
  () => controller.abort(new Error("restore timeout")),
  5_000,
);
try {
  const { text } = await shield.restore(untrustedStoredText, {
    signal: controller.signal,
  });
} finally {
  clearTimeout(timer);
}

An abort also cuts short any in-progress retry backoff. To identify your application in server-side logs, set appInfo: { name: "my-service", version: "2.1.0" } on the client — it is appended to the SDK's user-agent header.

Runtime support

The SDK depends only on Web Platform APIs — fetch, TextEncoder/TextDecoder, and Web Crypto (via jose). It touches no Node-only globals (Buffer/process), so it runs anywhere those standards exist:

| Runtime | Supported | | ------------------------------------------------ | ------------------------------- | | Node.js (^20.19.0 \|\| ^22.12.0 \|\| >=23.0.0) | Yes | | Edge runtimes / Cloudflare & Vercel Workers | Yes | | Deno / Bun | Yes | | Browsers & bundlers (Vite, esbuild, webpack) | Yes — but read the caveat below |

Run it server-side. The client signs requests with your RSA private key, so it belongs anywhere that key is a server secret — a Node route handler, a Server Component/Action, an edge function, a worker. Do not import it into a browser bundle or a "use client" component: that ships the private key to the client.

Next.js: works on both the Node.js runtime and the Edge runtime — route handlers, server components, server actions, and middleware are all fine. The only unsupported context is client components (see above).

The engines.node floor applies only to CommonJS require() callers (require(esm) needs Node ≥ 20.19 / 22.12 / 23); plain ESM import works on any modern runtime. CommonJS TypeScript consumers also need TypeScript ≥ 5.8 with module/moduleResolution: "nodenext" — on older TypeScript, use a dynamic import() instead.

Error handling

All SDK errors extend ShieldError:

| Error | Meaning | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ShieldError | base class; also thrown directly for client-side misconfiguration — e.g. a privateKeyPem that cannot be imported as a PKCS#8 RSA key for RS256 or cannot sign with it, with the underlying jose/WebCrypto error on cause | | ShieldApiError | non-2xx response; carries status, errorCode, raw body, and the gateway requestId (quote it to support) | | ShieldAuthError | gateway authorizer denied the JWT (SDK already retried once with a fresh mint) | | ShieldPayloadTooLargeError | 413 — body over the protect (500 KB = 500,000 bytes) / detokenize (512 KiB = 524,288 bytes) caps; also thrown client-side pre-flight | | ShieldResponseTooLargeError | 422 — the restored payload would exceed the response cap; split the batch | | ShieldNetworkError | no HTTP response (DNS/connection/timeout); underlying error on cause | | ShieldSpliceError | the server's findings violated invariants — the SDK fails loud rather than corrupt data | | ShieldTagValidationError | tag rejected client-side before any network call |

import { ShieldApiError, ShieldAuthError } from "@altrsoftware/shield";

try {
  await shield.protect(text);
} catch (err) {
  if (err instanceof ShieldAuthError) {
    // signing key not registered / rotated away — check key_registration slots
  } else if (err instanceof ShieldApiError) {
    console.error(err.toSafeString()); // "ShieldApiError: HTTP 400 (error_code 700400) [req abc-123]"
  }
  throw err;
}

Catch order: ShieldAuthError, ShieldPayloadTooLargeError, and ShieldResponseTooLargeError all extend ShieldApiError, so test the specific subclasses before the ShieldApiError base (as above) — a ShieldApiError branch placed first swallows all three.

Logging caution: ShieldApiError.message and .body reproduce the server response verbatim, which for validation errors can echo fragments of the submitted (sensitive) text. Use toSafeString() in logs and telemetry.

The numeric errorCode (present on apiError-shaped bodies) uses the 700xxx family — the last three digits mirror the HTTP status; full reference in docs/error-codes.md. A 401/403 whose body is not apiError-shaped is the gateway authorizer rejecting the JWT before it reached Shield — surfaced as ShieldAuthError after one fresh-bearer replay.

Learn more

See the repository's docs/ for the token-envelope grammar, the full usage guide, and the security notes (private-key handling, TLS enforcement, untrusted-input costs), and examples/ for runnable programs.

Support

New features and fixes land on the latest major version only; the supported Node.js range is the engines field. File bugs and feature requests on the repository issue tracker; report suspected vulnerabilities privately per SECURITY.md, never as public issues. Contributions are welcome — see CONTRIBUTING.md.

License

Apache-2.0 © ALTR Solutions, Inc.