@permanentlymobile/pm-privacy-guard
v1.0.0
Published
PM Privacy Guard -- AI message privacy routing, classification, and sanitization. Self-hosted. Zero-data-retention provider routing. One-time ownership.
Downloads
26
Maintainers
Readme
PM Privacy Guard
Protect sensitive data before it reaches AI providers.
PM Privacy Guard classifies, sanitizes, and routes messages based on sensitivity. PII, financial data, medical records, crypto wallets, legal content -- all detected and handled automatically before anything leaves your application.
What It Does
- Sensitivity Classification -- Scores every message on a 0-1 scale across 5 built-in categories (PII, financial, medical, legal, crypto) plus a personal-names category, with an optional
customPatternsslot per category - Automatic Sanitization -- Redacts, masks, or tokenizes sensitive content; tokenize mode returns a reversible token map
- Privacy Routing -- Returns a
recommendedProviderofprivacy,standard, orblockbased on score thresholds. Your application mapsprivacyto whichever zero-retention provider you have configured (e.g. Venice, Ollama) - Provider Freedom -- Sanitization happens before the prompt leaves your machine, so the downstream model becomes a cost-and-quality decision rather than a data-exposure one. Capable low-cost and free models -- DeepSeek and other open-weight options that now rival GPT and Claude on many tasks, including providers hosted in other jurisdictions -- become usable for sensitive work, because the personal data was already stripped upstream
- Intent Detection -- Classifies each message as
agent(needs tool execution),privacy(privacy keywords or sensitive content), orchat(safe for standard providers) - Audit Logging -- Append-only SQLite audit trail with CSV export, encrypted at rest with SQLCipher when a passphrase is configured
- Context & Prompt Sanitization -- Strips sensitive content from memory context and system prompts before they reach external providers
- Per-Chat Privacy Levels -- Override routing behavior per conversation (off, keywords, content, full)
Scope and Limitations
PM Privacy Guard defends against accidental disclosure -- personal or sensitive data that would otherwise reach an external LLM without you intending it. "Accidental" is broad on purpose. It includes data you never typed by hand: a pasted spreadsheet or document, content copied out of a PDF, OCR output, text from another application, or digits entered through a non-Latin keyboard. Privacy Guard classifies the entire outbound prompt, not only your last keystroke, so PII arriving in a format you never chose is exactly what it is built to catch.
It is built for individuals and small teams who want a practical privacy layer without enterprise overhead.
What it is not. No content classifier is a 100% guarantee, and Privacy Guard does not claim to be one. In normal use it catches the large majority of accidental disclosures, and it is most effective combined with good habits -- review what you paste, and prefer your privacy-routed provider for anything sensitive.
The deliberate-evasion caveat. Privacy Guard does not defend against a user intentionally obfuscating their own data to slip it past the classifier -- for example, splicing stray letters through their own Social Security number. Bypassing your own privacy tool is a choice, not an accident, and it is outside what this product is designed to stop. Privacy Guard protects you from mistakes, not from yourself on purpose.
Quick Start
PM Privacy Guard ships as plain JavaScript. No build step, no bytecode loader, no native compilation outside better-sqlite3-multiple-ciphers (used only when audit logging is enabled).
# 1. Install dependencies (Node >= 20 required).
# Use `npm ci` for a deterministic install from the committed lockfile;
# plain `npm install` will work but may pull in newer transitive versions.
# If you're on Node < 20, upgrade first -- better-sqlite3-multiple-ciphers
# will fail to build on older Node releases.
npm ci
# 2. Create your config (interactive wizard or copy the example).
# If you skip this step and run pm-privacy-guard.mjs without a
# config.yaml, the engine prints a one-line hint telling you to
# run the wizard, then falls back to safe defaults with an
# unencrypted audit log.
node setup-wizard.mjs
# OR: cp config.example.yaml config.yaml && editThe wizard now asks two new questions in addition to the existing sensitivity / provider setup:
- Which AI harness will you use Privacy Guard with?
claude-code | openai-shape | anthropic-direct | custom - Privacy backend: local Ollama (free, slower) or Venice (paid, faster)?
Default:
local.
Both answers are persisted as harness and privacy_backend in
config.yaml and honored at runtime.
# 3. Test with demo mode
node pm-privacy-guard.mjs --demo
# 4. Start with your config
node pm-privacy-guard.mjsIntegration
import { PrivacyShield } from './lib/privacy/shield.js';
const shield = await new PrivacyShield({
personalNames: ['John', 'Jane'],
defaultMode: 'redact',
sensitivityThreshold: 0.4,
blockThreshold: 0.9,
auditEnabled: true,
dbPath: './store/pm-privacy-guard.db',
providers: [
{ name: 'venice', type: 'privacy' },
{ name: 'openai', type: 'standard' },
],
}).init();
// Classify a message
const result = shield.classify("My SSN is 123-45-6789");
// { score: 0.6, categories: ['pii'], matches: [...] }
// Route a message (classify + sanitize + decide provider)
const decision = shield.route("Send payment to 0xABC...");
// { intent: 'privacy', sensitivity: {...}, recommendedProvider: 'privacy', sanitized: {...} }
// Sanitize content (default mode is "redact")
const clean = shield.sanitize("Card number 4532-1234-5678-9012");
// { sanitized: "Card number [REDACTED-FINANCIAL]", redactions: 1 }
// Tokenize mode also returns a reversible token map
const tokenized = shield.sanitize("My SSN is 123-45-6789", { mode: 'tokenize' });
// { sanitized: "My SSN is [TOKEN_001]", redactions: 1, tokenMap: Map{...} }
// Reverse tokenization (only meaningful for tokenize mode)
const original = shield.detokenize(tokenized.sanitized, tokenized.tokenMap);
// Clean up
shield.close();Harnesses
PM Privacy Guard is harness-agnostic. The same engine sits in front of whatever AI harness you ship with. A harness adapter does the scrub on the way out and the detokenization on the way back; the call site keeps using whichever SDK it already imports.
| Harness | Targets | Adapter import |
|--------------------|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| claude-code | Anthropic's Claude Code SDK (@anthropic-ai/claude-code) | import { ClaudeCodeAdapter } from './lib/privacy/harness/claude-code.js'; |
| openai-shape | Cursor, Windsurf, Aider, openai-node, Groq, Together, Mistral, any OpenAI-compatible chat.completions | import { OpenAIShapeAdapter } from './lib/privacy/harness/openai-shape.js'; |
| anthropic-direct | Raw @anthropic-ai/sdk | import { AnthropicDirectAdapter } from './lib/privacy/harness/anthropic-direct.js'; |
| custom | Bring-your-own SDK (subclass BaseHarnessAdapter) | import { BaseHarnessAdapter } from './lib/privacy/harness/base.js'; |
Wiring is the same shape for every adapter. The shield exposes
wrap(), unwrap(), and interceptStream() that route through the
adapter selected by config.harness -- swap the config value and the
runtime path swaps with it. There is no manual selectAdapter step in
the integrator's hot path.
shield.wrap() returns { messages, ctx }. The integrator threads the
ctx back into shield.unwrap(ctx, response) /
shield.interceptStream(ctx, stream) so concurrent requests on a
single shared PrivacyShield instance cannot cross-contaminate their
token maps. The shield itself holds no per-request state.
import { PrivacyShield } from './lib/privacy/shield.js';
const shield = await new PrivacyShield(config).init();
// Before the SDK call -- routed through the configured harness adapter.
// wrap() returns { messages, ctx }: the ctx carries this request's
// token map and must travel back to unwrap() / interceptStream().
const { messages: wrappedMessages, ctx } = shield.wrap(messages);
// Your usual SDK call (example: openai-node)
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: wrappedMessages,
});
// Reverse tokenization in the reply -- pass the same ctx back in.
const clean = shield.unwrap(ctx, response);
// Streaming
for await (const chunk of shield.interceptStream(ctx, openaiStream)) {
process.stdout.write(chunk.choices[0].delta.content ?? '');
}When config.harness is claude-code, shield.wrap() runs the
ClaudeCodeAdapter; flip it to openai-shape and the same call routes
through OpenAIShapeAdapter (no other code change needed). The adapter
is constructed lazily on the first wrap()/unwrap() and memoized for
the lifetime of the shield. The adapter holds no per-request state --
all per-request data lives in the ctx object the integrator threads
explicitly -- so a single shared PrivacyShield is safe for any
number of overlapping in-flight requests. If you need direct adapter
access (e.g. to construct multiple adapters with different modes),
selectAdapter is still exported from lib/privacy/harness/index.js.
If you need a harness that's not listed, subclass BaseHarnessAdapter
and override _scrubMessage / _readResponseText / _writeResponseText
for your SDK's shape -- the scrub logic is inherited and each text block
in an array-shape message is scrubbed independently so multi-part
messages keep their per-block boundaries.
Privacy backend
Some privacy work needs an LLM (prompt rewrites, tokenization round-trips). The sensitivity classifier itself is regex-only and stays local; only the LLM-driven paths hit a backend. PM Privacy Guard ships with two backend implementations and a runtime selector.
| Backend | Endpoint | Cost | Notes |
|----------|-----------------------------------------|-------|-----------------------------------------------------------------------|
| local | http://localhost:11434 (Ollama) | Free | No network egress. Slower. Configure model via OLLAMA_MODEL. |
| venice | https://api.venice.ai/api/v1 | Paid | Zero-retention. Faster. Needs VENICE_API_KEY. Default model llama-3.3-70b. |
Pick one in config.yaml:
privacy_backend: local # or 'venice'Or pick programmatically:
import { selectBackend } from './lib/privacy/backend/index.js';
const backend = selectBackend({ privacy_backend: 'venice' });
const { content } = await backend.chat([
{ role: 'user', content: 'Rewrite this sentence neutrally: ...' },
]);Inside an initialized PrivacyShield, shield.getBackend() returns the
same instance and adapters use it automatically.
The Venice backend is the commercial privacy tier on top of the same engine: every classification, sanitization, and routing decision runs through the identical local pipeline; Venice only carries the LLM round-trip when one is needed. The local Ollama backend exists so the engine has a free, network-egress-free fallback that proves the abstraction. The Venice integration exists so teams that don't want to run their own model can hand the inference half to a zero-retention commercial provider without giving up source-readable controls.
Sensitivity Categories
| Category | Detects | |----------|---------| | PII | SSNs, addresses, phone numbers, names, emails, dates of birth | | Financial | Credit cards, bank accounts, routing numbers, balances, transactions | | Medical | Diagnoses, medications, conditions, medical record numbers | | Legal | Case numbers, attorney-client privilege markers, court dockets | | Crypto | Wallet addresses (ETH, BTC, SOL), private keys, seed phrases |
Sanitization Modes
| Mode | Behavior | Example |
|------|----------|---------|
| redact | Replace with category tag | [REDACTED-PII] |
| mask | Partial masking | 123-**-**** |
| tokenize | Replace with reversible token | [TOKEN-a7f3b2] (can be reversed with token map) |
Configuration
Copy config.example.yaml to config.yaml and customize:
# Personal names to detect and protect
personal_names:
- John
- Jane
# Sanitization mode: redact | mask | tokenize
default_mode: redact
# Sensitivity threshold (0.0 - 1.0)
sensitivity_threshold: 0.4
# Block threshold (messages above this are blocked)
block_threshold: 0.9
# Your AI providers
providers:
- name: venice
type: privacy # zero-data-retention
- name: ollama
type: local # runs locally
- name: openai
type: standard # external, needs protection
# AI harness Privacy Guard sits in front of
# (claude-code | openai-shape | anthropic-direct | custom)
harness: claude-code
# Privacy backend for LLM-driven privacy work (local Ollama or Venice)
privacy_backend: local
# Audit logging
audit:
enabled: true
db_path: ./store/pm-privacy-guard.db
# SQLCipher key for the audit DB (written by the setup wizard).
# Omit the entire audit_log block to run the audit DB unencrypted.
audit_log:
passphrase: "<32-char alphanumeric, generated by setup-wizard.mjs>"Backups and the passphrase
When the setup wizard's Encrypt the local audit database at rest? (Y/n)
prompt is answered Y (the default), it generates a 32-char alphanumeric
passphrase and writes it to config.yaml under audit_log.passphrase.
The audit-logger module issues PRAGMA key = '...' immediately after
opening the database, so the on-disk .db file is SQLCipher-encrypted
from byte zero.
A few things to know:
- The passphrase lives in
config.yaml. It is never transmitted, never re-shown after the wizard prints it the one time. If you loseconfig.yaml, you lose the ability to read the audit log -- there is no recovery path baked into PM Privacy Guard. - Back up
config.yamlseparately fromstore/pm-privacy-guard.db. Keeping both backups in the same place (same drive, same archive, same cloud bucket) is equivalent to no encryption at rest -- a compromise of that location yields key + ciphertext together. Store the config in a password manager, a sealed envelope, or your team's secret store, distinct from wherever the DB lives. - There is no recovery path if both are lost. The audit log is designed to be tamper-evident and append-only; that means no backdoor. If you lose the passphrase, the audit history is gone.
- Rotating the passphrase (manual, v1). SQLCipher exposes
PRAGMA rekey = '...'which re-encrypts every page with a new key. To rotate, open the DB with the current key, issuePRAGMA rekey = '<new key>', then updateaudit_log.passphraseinconfig.yamlto match. PM Privacy Guard does not automate this in v1 -- operators handle it as a deliberate one-time event so the new key never accidentally diverges from the config file. - Running without encryption. Omit the entire
audit_logblock fromconfig.yamland the audit logger opens the file as plain SQLite. The CLI prints a yellow warning at startup (audit-logger running unencrypted because config.yaml is missing or has no audit_log.passphrase) so this can't happen silently.
Stuck migration lockfile
The first time PM Privacy Guard opens an audit DB with a passphrase
configured, it migrates any pre-existing plain SQLite file to
SQLCipher in place (the original is preserved alongside as
<db>.pre-encrypt.bak). The migration is serialized through an
exclusive lockfile at <db>.migrate.lock so two starting processes
cannot both rewrite the file. In a normal startup the lockfile is
created, held for the duration of the migration, and removed
automatically when the constructor returns.
In two scenarios startup will refuse rather than reclaim:
- A live holder's lockfile is older than 5 minutes. The error message names the holder PID and tells you which file to remove.
- The lockfile carries a different
hostname(e.g. another container shares the volume). PID liveness cannot be probed across PID namespaces, so reclaim is unsafe. The error message names the foreign hostname.
To force-unlock manually:
# Confirm no other PM Privacy Guard process is actually mid-migration:
# - on this host: `ps -p <holderPid>` shows nothing, OR
# - on another host: ask the other operator before doing anything.
rm <db_path>.migrate.lockForce-unlocking while another process is genuinely mid-migration can
corrupt the plain-to-encrypted rewrite and the .pre-encrypt.bak
recovery file. Only remove the lockfile when you are confident no
migration is in progress -- either because the named PID is gone or
because you have confirmed with the operator of the other host. If
you remove the lockfile in error and the migration ends up corrupt,
restore from <db>.pre-encrypt.bak (if present) and re-run startup.
License
Privacy Guard ships as one license, all features. License keys are HMAC-signed JWTs that carry the licensee, a provider cap, the feature list, and optional expiry. A valid key advertises every feature this engine exposes:
- Sensitivity classification (6 categories)
- Data sanitization (redact / mask / tokenize)
- Privacy routing
- Audit logging (SQLite) and CSV export
- Context sanitization
- Prompt sanitization
- Custom detection patterns
- Multi-tenant routing
- Up to 25 registered providers
Generate or validate keys with license-tool.mjs. Set PRIVACY_SHIELD_LICENSE_SECRET in the environment to the same secret used at generation time.
CLI Commands
# Run with config
node pm-privacy-guard.mjs
# Demo mode (sample classifications)
node pm-privacy-guard.mjs --demo
# Show current config info
node pm-privacy-guard.mjs --info
# Setup wizard
node setup-wizard.mjs
# License management
node license-tool.mjs generate --tier standard --type perpetual --holder "[email protected]"
node license-tool.mjs validate <key>
node license-tool.mjs info <key>API Reference
new PrivacyShield(config).init()
Creates and initializes a PM Privacy Guard instance. init() is async and loads optional modules (audit logger).
.classify(message) -> SensitivityResult
Returns { score, categories, matches } for any text input.
.sanitize(message, options?) -> SanitizeResult
Returns { sanitized, redactions } for redact and mask mode. In tokenize mode the result also includes tokenMap: Map<string,string>. Pass { mode: 'mask' } or { mode: 'tokenize' } to override the default mode. There is no categories field on the sanitize result -- categories are returned by .classify().
.route(message, options?) -> RoutingDecision
Full pipeline: classify, sanitize if needed, return routing decision with { intent, sensitivity, recommendedProvider, sanitized, auditId }. intent is one of 'agent' | 'privacy' | 'chat'. recommendedProvider is one of 'privacy' | 'standard' | 'block'.
.sanitizeContext(memoryBlock, provider) -> string
Strip sensitive content from memory context blocks before sending to a specific provider.
.sanitizeMessage(content, provider) -> string
Sanitize message content for a specific provider.
.sanitizePrompt(systemPrompt, provider) -> string
Strip configured sections from system prompts for external providers.
.setPrivacyLevel(chatId, level) / .getPrivacyLevel(chatId)
Per-chat privacy level override. Levels: off, keywords, content (default), full.
.getAuditStats(chatId?, since?) -> AuditStats
Audit statistics. Filter by chat ID and/or timestamp. Returns the empty-stats object when audit logging is disabled or unavailable.
.getAuditEntries(chatId, limit?) -> AuditEntry[]
Raw audit log entries for a specific chat. Returns [] when audit logging is disabled or unavailable.
.exportAuditCSV(since) -> string
Export audit log as CSV from a given timestamp. Returns "" when audit logging is disabled or unavailable.
.close()
Clean shutdown. Closes database connections.
For AI Agents
Plain-text API reference for LLM and AI agent integration. PM Privacy Guard is a JavaScript library (not an HTTP server). Import and call methods directly in your Node.js application.
Library API
Import: import { PrivacyShield } from './lib/privacy/shield.js';
Initialize: const shield = await new PrivacyShield(config).init();
Constructor config object: { personalNames: string[], // names to detect and protect (default: []) defaultMode: string, // "redact" | "mask" | "tokenize" (default: "redact") sensitivityThreshold: number, // 0.0-1.0, route to privacy above this (default: 0.4) blockThreshold: number, // 0.0-1.0, block messages above this (default: 0.9) auditEnabled: boolean, // enable SQLite audit logging (default: true) dbPath: string, // path to audit database (default: "./store/pm-privacy-guard.db") providers: [ // AI providers for routing decisions { name: "venice", type: "privacy" }, { name: "ollama", type: "local" }, { name: "openai", type: "standard" } ], stripSections: string[], // system prompt sections to strip for external providers customPatterns: object // custom detection patterns: { [category]: RegExp[] } }
Methods
shield.classify(message: string) -> SensitivityResult Classify text for sensitive content. Returns: { score: number, // 0.0-1.0 sensitivity score categories: string[], // any of ["pii", "financial", "medical", "legal", "crypto", "personal"] detections: [ // individual matches { category: string, pattern: string, match: string, confidence: number, position: { start: number, end: number } } ], recommendation: string // "standard" | "privacy" | "block" }
shield.sanitize(message: string, options?: object) -> SanitizeResult
Sanitize sensitive content. Classifies first, then replaces detected spans.
Options: { mode?: "redact" | "mask" | "tokenize", categories?: string[], preserveStructure?: boolean }
Returns (redact, mask):
{
sanitized: string, // cleaned text
redactions: number // count of replacements made
}
Returns (tokenize):
{
sanitized: string,
redactions: number,
tokenMap: Map<string,string> // reversible token map; pass back to detokenize()
}
Note: there is no categories field on the sanitize result. Use shield.classify() to get categories.
shield.detokenize(sanitized: string, tokenMap: Map) -> string Reverse tokenization. Restores original values from token map.
shield.route(message: string, options?: object) -> RoutingDecision Full pipeline: classify + sanitize + route. Options: { chatId?: string, targetProvider?: string } Returns: { intent: string, // "agent" | "privacy" | "chat" sensitivity: SensitivityResult, recommendedProvider: string, // "privacy" | "standard" | "block" sanitized: SanitizeResult | undefined, auditId: number | undefined } Notes: - intent="agent" -> message likely needs tool execution in your app - intent="privacy" -> privacy keywords detected or sensitivity recommendation is "privacy"/"block" - intent="chat" -> safe-by-default routing - recommendedProvider is independent of intent and reflects threshold + per-chat level.
shield.sanitizeContext(memoryBlock: string, targetProvider: string) -> string Strip sensitive content from memory context blocks before sending to a provider.
shield.sanitizeMessage(content: string, targetProvider: string) -> string Sanitize message content for a specific provider.
shield.sanitizePrompt(systemPrompt: string, targetProvider: string) -> string Strip configured sections from system prompts for external providers.
shield.setPrivacyLevel(chatId: string, level: string) Set per-chat privacy level. Levels: "off" | "keywords" | "content" | "full"
- off: no privacy enforcement, route everything to standard
- keywords: only route on explicit privacy keywords
- content: route based on classifier score (default)
- full: always route to privacy provider
shield.getPrivacyLevel(chatId: string) -> string Get per-chat privacy level. Returns "content" if not set.
shield.getAuditStats(chatId?: string, since?: number) -> AuditStats Returns: { totalRequests, privacyRouted, standardRouted, blocked, topCategories, averageSensitivityScore } Returns the empty-stats object if audit logging is disabled or unavailable.
shield.getAuditEntries(chatId: string, limit?: number) -> AuditEntry[] Raw audit log entries for a specific chat. Default limit 50. Returns [] if audit logging is disabled or unavailable.
shield.exportAuditCSV(since: number) -> string Export audit entries as CSV from a Unix epoch timestamp. Returns "" if audit logging is disabled or unavailable.
shield.info() -> object Returns: { version, personalNamesConfigured, defaultMode, sensitivityThreshold, blockThreshold, auditEnabled, providers, privacyProviders, activeSessions }
shield.close() Clean shutdown. Closes database connections. Call before process exit.
CLI Commands
node pm-privacy-guard.mjs Start with config.yaml node pm-privacy-guard.mjs --demo Run demo classification on sample inputs node pm-privacy-guard.mjs --info Show current configuration node setup-wizard.mjs Interactive setup, writes config.yaml node license-tool.mjs generate --tier standard --type perpetual --holder "email" node license-tool.mjs validate node license-tool.mjs info
Config Schema (config.yaml)
personal_names: string[] # names to detect and protect default_mode: string # redact | mask | tokenize sensitivity_threshold: number # 0.0-1.0 (default 0.4) block_threshold: number # 0.0-1.0 (default 0.9) providers: - name: string # provider name type: string # "privacy" | "local" | "standard" strip_sections: string[] # system prompt sections to remove for external providers harness: string # claude-code | openai-shape | anthropic-direct | custom (default: claude-code) privacy_backend: string # local | venice (default: local) audit: enabled: boolean db_path: string # path to SQLite audit database audit_log: # optional; omit the block to disable at-rest encryption passphrase: string # 32-char alphanumeric, written by setup-wizard.mjs license: string # license key (optional)
Workflow
- npm install
- node setup-wizard.mjs (or cp config.example.yaml config.yaml and edit)
- In your application code: a. Import and initialize PrivacyShield b. Before sending any user message to an external AI provider, call shield.route(message) c. If recommendedProvider is "block", reject the message d. If recommendedProvider is "privacy", send to a zero-retention provider (Venice, Ollama) e. If recommendedProvider is "standard", safe to send to any provider f. Use the sanitized output from the route() result when sending to external providers
- Call shield.close() on process exit
Protocol and Recipes
Classify and route a message before sending to an LLM:
const decision = shield.route(userMessage); if (decision.recommendedProvider === 'block') { return 'Message blocked: contains highly sensitive content.'; } const textToSend = decision.sanitized ? decision.sanitized.sanitized : userMessage; const provider = decision.recommendedProvider; // "privacy" or "standard" // Send textToSend to the appropriate provider
Sanitize and restore (round-trip tokenization):
const result = shield.sanitize("My SSN is 123-45-6789", { mode: "tokenize" }); // result.sanitized = "My SSN is [TOKEN-a7f3b2]" // Send result.sanitized to LLM, get response back const restored = shield.detokenize(llmResponse, result.tokenMap); // Original values restored in the response
Batch-sanitize messages before sending context to an external provider:
const messages = [ { role: 'user', content: 'My balance is $50,000' }, { role: 'assistant', content: 'I can help with that.' }, ]; const cleaned = messages.map(m => ({ ...m, content: shield.sanitizeMessage(m.content, 'openai'), }));
Per-chat privacy override (force all messages through privacy provider):
shield.setPrivacyLevel('chat-123', 'full'); const decision = shield.route("any message", { chatId: 'chat-123' }); // decision.recommendedProvider will always be "privacy"
Requirements
- Node.js >= 20.0.0
- npm
Security
PM Privacy Guard runs entirely on your machine. No data is sent to any external service by this module. The routing decisions are recommendations for your application to act on -- PM Privacy Guard itself never transmits data.
PM Privacy Guard ships as plain JavaScript. Source is readable. License keys are HMAC-signed JWTs that authenticate the licensee. Customers, security reviewers, and auditors can read every line of the engine before it touches their data.
If a deployment requires source-level obfuscation, wrap PM Privacy Guard inside your own bundler (esbuild, webpack, ncc) under your own pipeline -- the package does not ship a bytecode build itself.
Support
- Documentation: products.permanentlymobile.com/pm-privacy-guard
- Issues: Contact support via the product page
Built by Permanently Mobile
