Node.js SDK

@nopii/node is the server half of a protected app. It mints sessions for the browser, points your LLM client at the NoPII proxy, and tokenizes text your server handles directly.

Preview

The SDKs are in preview and not yet published to npm or PyPI. Package names and APIs may change before 1.0.

Install

bash
npm install @nopii/node
typescript
import { NoPIIServer } from "@nopii/node";

const nopii = new NoPIIServer({ secretKey: process.env.NOPII_SECRET_KEY! });

Use a secret key (nsk_...) and keep it on the server. The constructor refuses a publishable key. One instance is safe to share across concurrent requests.

Mint client sessions

Add a route your frontend calls for a session. Bind it to your own user id: that is what lets the user reveal their earlier messages after a reload or on another device, and what keeps other users from revealing them.

typescript
// app/api/nopii-session/route.ts (Next.js)
export async function POST(req: Request) {
  const user = await requireUser(req); // your auth
  const session = await nopii.clientSessions.create({ endUserId: user.id, ttlSeconds: 900 });
  return Response.json({ token: session.token, expiresAt: session.expiresAt });
}

Sessions last 15 minutes by default and at most an hour. The browser asks again when one is about to expire.

Forward messages to the LLM

The browser sends your route tokens. Forward them through the proxy in tokens-only mode: the model is told what the tokens stand for, and the reply comes back tokenized for the browser to reveal.

Vercel AI SDK

typescript
import { createOpenAI } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json(); // already tokenized in the browser
  const openai = createOpenAI({
    apiKey: process.env.OPENAI_API_KEY,
    ...nopii.proxySettings({ request: req, mode: "tokens-only" }),
  });
  const result = streamText({
    model: openai("gpt-4o"),
    messages: await convertToModelMessages(messages),
  });
  return result.toUIMessageStreamResponse();
}

proxySettings reads the X-NoPII-Session-Id header the browser SDK sent, so tokens the proxy creates for anything the browser missed stay revealable by the same user. Both openai(...), which uses the Responses API, and openai.chat(...) work. For Anthropic, spread the same settings into createAnthropic.

Official OpenAI and Anthropic clients

typescript
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  ...nopii.openAIClientOptions({ request: req, mode: "tokens-only" }),
});

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
  ...nopii.anthropicClientOptions({ request: req, mode: "tokens-only" }),
});

Use the helper that matches the client

The Anthropic client adds /v1/messages itself, while OpenAI clients expect /v1 in the base URL. Mixing them up sends requests to /v1/v1/messages.

Sessions without a request object

Deep inside a job or a helper, wrap the work in withSession. Every proxy call made through the SDK's fetch inside it, including in nested async code, carries that session. Concurrent requests keep their own.

typescript
const openai = new OpenAI({ apiKey, ...nopii.openAIClientOptions() });

await nopii.withSession(sessionId, () => summarize(openai, thread), "tokens-only");

Tokenize on the server

For text that never passes through the proxy, such as documents you index for retrieval or records you import:

typescript
const result = await nopii.tokenize(["Contract signed by Wei Chen"], { endUserId: customer.id });
result.texts[0]; // "Contract signed by [NAME: ...]"

const { entities } = await nopii.detect(["Is there PII in here?"]); // not tokenized, not metered

Pass endUserId for data that belongs to one of your users, so that user can reveal it later and no one else can.

Reveal grants for support tools

A secret key does not reveal another user's tokens by default. When a support agent needs to read a customer's conversation, create a short-lived grant and hand its id to the agent's browser.

typescript
const grant = await nopii.revealGrants.create({
  subjectEndUserId: ticket.customerId,
  granteeEndUserId: agent.id,
  ttlSeconds: 900,
  reason: `ticket ${ticket.id}`,
});

// In the agent's browser: nopii.reveal(text, { grantId: grant.id })
await nopii.revealGrants.revoke(grant.id); // when the ticket closes

Every reveal under a grant is logged. Your own server can use a grant with no grantee: nopii.detokenize(tokens, { grantId }).

Reference

MemberDoes
clientSessions.create({ endUserId, ttlSeconds })Mints a session token for a browser or app
proxySettings({ request, sessionId, mode })baseURL, headers, and fetch for AI SDK providers
openAIClientOptions(...)Options for new OpenAI
anthropicClientOptions(...)Options for new Anthropic
withSession(sessionId, fn, mode)Applies a session to proxy calls made inside fn
tokenize(texts, { endUserId, sessionId })Detects and tokenizes
detect(texts)Detects only
detokenize(tokens, { sessionId, grantId })Reveals under the reveal policy
revealGrants.create(...) / revoke(id)Manages reveal grants

Related