{"_id":"@bastionsoft/prompt-protection","_rev":"2-0d6e21eec61a9eec6e480c6644614829","name":"@bastionsoft/prompt-protection","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@bastionsoft/prompt-protection","version":"0.1.0","keywords":["prompt-injection","jailbreak","llm-security","ai-safety","guardrails"],"author":{"name":"Bastion Soft"},"license":"AGPL-3.0-or-later","_id":"@bastionsoft/prompt-protection@0.1.0","maintainers":[{"name":"jev1234","email":"admin@bastionsoft.com"}],"homepage":"https://github.com/bastion-soft/bastion-prompt-protection-ts","bugs":{"url":"https://github.com/bastion-soft/bastion-prompt-protection-ts/issues"},"dist":{"shasum":"95ae334525e964e929cb5303f6176a54537c99aa","tarball":"https://registry.npmjs.org/@bastionsoft/prompt-protection/-/prompt-protection-0.1.0.tgz","fileCount":17,"integrity":"sha512-4NrdRHNqigJ5ujJ1i2wFVkpiUVISRCFpbTu+WP1EnteNyYK0C4vjkcJ8IMmbw2n0jipswirOuBW3lG/u8uds7A==","signatures":[{"sig":"MEQCID64DANxrisdj6mBXsT0Q4OMqXk1efRuI5IUsRUDYegdAiA2kRPLxNq7+Y7Nf3uOrQ8EIuvQDkZgrtnfTQC6EwZgAg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":337967},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"2cebca4c1c492919b2d9bbcfd12b4e919e11417b","scripts":{"lint":"eslint .","test":"vitest run --exclude '**/parity.test.ts' --exclude '**/known-deviations.test.ts' --exclude '**/protect-chunked.test.ts' --exclude '**/loader-concurrency.test.ts'","build":"tsup","format":"prettier --write .","prepack":"npm run build","lint:fix":"eslint . --fix","test:all":"vitest run","typecheck":"tsc --noEmit","test:watch":"vitest","test:parity":"vitest run test/parity.test.ts test/known-deviations.test.ts test/protect-chunked.test.ts test/loader-concurrency.test.ts","format:check":"prettier --check ."},"_npmUser":{"name":"jev1234","email":"admin@bastionsoft.com"},"repository":{"url":"git+https://github.com/bastion-soft/bastion-prompt-protection-ts.git","type":"git"},"_npmVersion":"11.12.1","description":"Local prompt injection and jailbreak detection for LLM applications","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@huggingface/hub":"^2.15.0","onnxruntime-node":"1.26.0","@huggingface/tokenizers":"^0.1.3"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","eslint":"^9.0.0","vitest":"^3.2.4","prettier":"^3.4.0","langsmith":"^0.8.10","@eslint/js":"^9.0.0","typescript":"^5.8.3","@types/node":"^22.15.0","typescript-eslint":"^8.0.0","eslint-config-prettier":"^10.0.0","@opentelemetry/resources":"^2.10.0","@opentelemetry/sdk-trace-node":"^2.10.0","@opentelemetry/exporter-trace-otlp-http":"^0.221.0"},"peerDependencies":{"langsmith":"*","@opentelemetry/resources":"*","@opentelemetry/sdk-trace-node":"*","@opentelemetry/exporter-trace-otlp-http":"*"},"peerDependenciesMeta":{"langsmith":{"optional":true},"@opentelemetry/resources":{"optional":true},"@opentelemetry/sdk-trace-node":{"optional":true},"@opentelemetry/exporter-trace-otlp-http":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/prompt-protection_0.1.0_1786967822587_0.9189921270443524","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bastionsoft/prompt-protection","version":"0.1.1","description":"Local prompt injection and jailbreak detection for LLM applications","keywords":["prompt-injection","jailbreak","llm-security","ai-safety","guardrails"],"homepage":"https://github.com/bastion-soft/bastion-prompt-protection-ts","bugs":{"url":"https://github.com/bastion-soft/bastion-prompt-protection-ts/issues"},"repository":{"type":"git","url":"git+https://github.com/bastion-soft/bastion-prompt-protection-ts.git"},"license":"AGPL-3.0-or-later","author":{"name":"Bastion Soft"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"engines":{"node":">=20"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","lint":"eslint .","lint:fix":"eslint . --fix","format":"prettier --write .","format:check":"prettier --check .","test":"vitest run --exclude '**/parity.test.ts' --exclude '**/known-deviations.test.ts' --exclude '**/protect-chunked.test.ts' --exclude '**/loader-concurrency.test.ts'","test:watch":"vitest","test:parity":"vitest run test/parity.test.ts test/known-deviations.test.ts test/protect-chunked.test.ts test/loader-concurrency.test.ts","test:all":"vitest run","prepack":"npm run build"},"dependencies":{"@huggingface/hub":"^2.15.0","@huggingface/tokenizers":"^0.1.3","onnxruntime-node":"1.26.0"},"peerDependencies":{"@opentelemetry/exporter-trace-otlp-http":"*","@opentelemetry/resources":"*","@opentelemetry/sdk-trace-node":"*","langsmith":"*"},"peerDependenciesMeta":{"@opentelemetry/exporter-trace-otlp-http":{"optional":true},"@opentelemetry/resources":{"optional":true},"@opentelemetry/sdk-trace-node":{"optional":true},"langsmith":{"optional":true}},"devDependencies":{"@eslint/js":"^9.0.0","@opentelemetry/exporter-trace-otlp-http":"^0.221.0","@opentelemetry/resources":"^2.10.0","@opentelemetry/sdk-trace-node":"^2.10.0","@types/node":"^22.15.0","eslint":"^9.0.0","eslint-config-prettier":"^10.0.0","langsmith":"^0.8.10","prettier":"^3.4.0","tsup":"^8.3.0","typescript":"^5.8.3","typescript-eslint":"^8.0.0","vitest":"^3.2.4"},"gitHead":"c23ae5bdac40d48269080838a4055f91c2a3778a","_id":"@bastionsoft/prompt-protection@0.1.1","_nodeVersion":"22.23.2","_npmVersion":"12.0.2","dist":{"integrity":"sha512-BUC5H6n8vSCCXL/rnfuzV8lhol3xR28cQINfqnh9MHFOxK2q1yMPkNzD/yLQ27lPRqC2evtBp1gB5Btx0xUBpw==","shasum":"f24048b60388c97811012bb8a35caafda2018678","tarball":"https://registry.npmjs.org/@bastionsoft/prompt-protection/-/prompt-protection-0.1.1.tgz","fileCount":17,"unpackedSize":337967,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bastionsoft%2fprompt-protection@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDi9t4i1E9ozYD9Lv6aRXVUh2EQt4udv63XjYn3bBYtmwIgKSF5zLevAe7Y2+OEPb3OmfdlPl/aFDisgsaHtN5i3mE="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:de71745c-0917-4510-97d6-3572fd1e7e3f"}},"directories":{},"maintainers":[{"name":"jev1234","email":"admin@bastionsoft.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/prompt-protection_0.1.1_1786969108506_0.09791926582464683"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-17T11:57:01.460Z","modified":"2026-08-17T12:18:29.054Z","0.1.0":"2026-08-17T11:57:02.743Z","0.1.1":"2026-08-17T12:18:28.669Z"},"bugs":{"url":"https://github.com/bastion-soft/bastion-prompt-protection-ts/issues"},"author":{"name":"Bastion Soft"},"license":"AGPL-3.0-or-later","homepage":"https://github.com/bastion-soft/bastion-prompt-protection-ts","keywords":["prompt-injection","jailbreak","llm-security","ai-safety","guardrails"],"repository":{"type":"git","url":"git+https://github.com/bastion-soft/bastion-prompt-protection-ts.git"},"description":"Local prompt injection and jailbreak detection for LLM applications","maintainers":[{"name":"jev1234","email":"admin@bastionsoft.com"}],"readme":"# @bastionsoft/prompt-protection\n\n[![npm](https://img.shields.io/npm/v/@bastionsoft/prompt-protection)](https://www.npmjs.com/package/@bastionsoft/prompt-protection)\n[![CI](https://github.com/bastion-soft/bastion-prompt-protection-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/bastion-soft/bastion-prompt-protection-ts/actions/workflows/ci.yml)\n[![node](https://img.shields.io/node/v/@bastionsoft/prompt-protection)](https://www.npmjs.com/package/@bastionsoft/prompt-protection)\n[![license](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue)](LICENSE)\n\nPrompt injection and jailbreak detection for Node.js. Runs entirely in your\nprocess — a regex heuristics pass, then an ONNX DeBERTa-v3 classifier on CPU. No\nAPI calls, no data leaves the machine.\n\n```bash\nnpm install @bastionsoft/prompt-protection\n```\n\n```ts\nimport { Guard } from \"@bastionsoft/prompt-protection\";\n\nconst guard = new Guard();\n\nconst result = await guard.protect(\"Ignore all previous instructions.\");\n// { risk: 0.9853, label: \"attack\", stageReached: \"binary\", latencyMs: 32.6, isAttack: true }\n\nif (result.isAttack) {\n  // block, log, or route to a human\n}\n```\n\nConstruct `Guard` once at startup. The first `protect()` downloads ~98 MB of\nmodel weights into the standard HuggingFace cache (~2 s); after that a scan is\nsingle-digit milliseconds.\n\nAlso available for Python as\n[`bastion-prompt-protection`](https://pypi.org/project/bastion-prompt-protection/) —\nsame model, same thresholds, same scores.\n\n## Detection pipeline\n\n1. Truncate to `maxInputChars` (characters, not tokens).\n2. **Heuristics** — chat-template control tokens (`0.97`), fake end-of-prompt\n   delimiters (`0.90`), zero-width obfuscation (`0.96`), spaced letters (`0.80`),\n   base64 payloads (`0.55`).\n3. A heuristic score ≥ `0.95` returns immediately — **the model never loads**, so\n   structural attacks cost microseconds.\n4. Otherwise the ONNX classifier runs; `risk = max(heuristic, model)`.\n\nIf the weights can't be downloaded, the classifier reports itself unavailable and\nthe guard degrades to heuristics-only with a warning rather than throwing.\n\n## API\n\n### `new Guard(config?)`\n\n| Option                             |     Default | Meaning                                                      |\n| ---------------------------------- | ----------: | ------------------------------------------------------------ |\n| `preset`                           |    `\"tiny\"` | `\"tiny\"` (free, 70M) or `\"multilingual\"` (commercial, gated) |\n| `model`                            |           — | Any HuggingFace repo id; overrides `preset`                  |\n| `thresholds.attackAbove`           |       `0.5` | Risk at or above this is labelled `attack`                   |\n| `thresholds.heuristicShortCircuit` |      `0.95` | Heuristic score at or above this skips the model             |\n| `enableHeuristics`                 |      `true` | Run the regex stage                                          |\n| `enableBinary`                     |      `true` | Run the ONNX stage                                           |\n| `maxInputChars`                    |      `8000` | Characters kept before detection                             |\n| `cacheDir`                         |           — | Override the model cache location                            |\n| `hfToken`                          |           — | HuggingFace token; defaults to `$HF_TOKEN`                   |\n| `licensePath` / `requireLicense`   | — / `false` | Offline commercial-license checks                            |\n\n`protect(prompt)` resolves to\n`{ risk, label, stageReached, latencyMs, isAttack }`. `risk` is rounded to 4\ndecimals, `latencyMs` to 3.\n\nAlso on the instance: `guard.sdkVersion`, `guard.modelVersion` (7-character\nmodel-snapshot id, `null` until first use — worth recording in audit logs), and\n`guard.licenseStatus()`.\n\n### `protectChunked(prompt, options?)` — for documents and tool results\n\nThe classifier reads at most **512 tokens (~2,000 characters)**. Beyond that,\ntext is not weakly weighted — it is not read at all, so an injection at offset\n3,000 of a 20 KB file scores exactly the same as the clean file. Content the\nmodel _does_ read also gets diluted: a short payload inside a long benign\npassage is scored down.\n\nFor anything document-shaped, use `protectChunked()`. It splits on sentence and\nline boundaries and takes the worst verdict:\n\n```ts\nconst result = await guard.protectChunked(document);\n// { risk, label, isAttack, chunksScanned, chunksTotal, … }\n```\n\n| Option      |    Default | Meaning                                                     |\n| ----------- | ---------: | ----------------------------------------------------------- |\n| `minLen`    |      `120` | Merge lines/sentences until a chunk reaches this many chars |\n| `maxLen`    |      `400` | Split any single line/sentence longer than this             |\n| `overlap`   |        `0` | Repeat this many chars of the previous chunk                |\n| `maxChunks` | _no limit_ | Stop after this many chunks                                 |\n\nIt stops at the first chunk over the threshold, so **clean content is the\nexpensive case** — every chunk is scanned. Budget ~1.2 s per 20 KB. Compare\n`chunksScanned` with `chunksTotal` to tell whether the whole input was covered.\n\nDefaults are exported as `DEFAULT_CHUNK_OPTIONS`, along with\n`MODEL_TOKEN_WINDOW` and the `chunkContent()` splitter itself.\n\n> **On document content, raise the threshold.** The `0.5` default is calibrated\n> for chat prompts. On documents and tool results this model is materially more\n> trigger-happy — roughly one clean document in four at `0.5`. Around `0.9`\n> detects slightly _better_ on that content with a third fewer false positives.\n> Measure on your own traffic before enforcing. See\n> [docs/measurements.md](docs/measurements.md).\n\n### Running it off the main thread\n\n**`protect()` is CPU-bound and blocks the Node event loop** — most of it in the\ntokenizer, which is pure JavaScript and cannot be offloaded in place. In a\nserver or gateway, run it in a child process: measured event-loop availability\nduring sustained scanning goes from **15% to 82%**.\n\nA child process beats a worker thread here for two reasons: it contains a crash\nin the native ONNX Runtime, and it makes timeouts enforceable — you cannot abort\na native call, but you can kill a process holding one.\n\n```js\n// detector-child.mjs\nimport { Guard } from \"@bastionsoft/prompt-protection\";\nlet guard;\nlet queue = Promise.resolve(); // serialise: one model, one inference at a time\nprocess.on(\"message\", (req) => {\n  queue = queue.then(async () => {\n    try {\n      guard ??= new Guard();\n      const { risk, label } = await guard.protect(req.text);\n      process.send?.({ id: req.id, ok: true, risk, label });\n    } catch (err) {\n      process.send?.({ id: req.id, ok: false, error: String(err) });\n    }\n  });\n});\nprocess.once(\"disconnect\", () => process.exit(0));\n```\n\nIn the parent: keep a `Map` of pending request ids, enforce your own deadline\n(kill and respawn on expiry), bound the queue so it sheds load instead of\ngrowing, and reject pending requests when the child exits so callers fail\npredictably rather than hanging.\n\n### Telemetry\n\nOff by default — zero egress, no background timer. Opt in with a reporter:\n\n```ts\nimport {\n  Guard,\n  ReportingGuard,\n  buildReporter,\n  telemetryConfigFromEnv,\n} from \"@bastionsoft/prompt-protection\";\n\nconst reporter = await buildReporter(telemetryConfigFromEnv());\nconst guard = new ReportingGuard(new Guard(), reporter);\n```\n\nChannels: native HTTP (`BASTION_TELEMETRY_ENDPOINT` + `BASTION_TELEMETRY_KEY`),\nOTLP (`BASTION_OTEL_ENDPOINT`), and LangSmith (`BASTION_LANGSMITH`). The OTel and\nLangSmith packages are optional peer dependencies, imported lazily. Reporting is\nfire-and-forget: it never adds latency to, or throws into, the detection path.\n\n## Editions\n\n|           | **Free** (this package)         | **Commercial**                                        |\n| --------- | ------------------------------- | ----------------------------------------------------- |\n| Model     | `tiny` — DeBERTa-v3-xsmall, 70M | `multilingual` — mdeberta-v3-base, 280M               |\n| Languages | English                         | + German, French, Spanish, Italian, Norwegian, Danish |\n| License   | AGPL-3.0                        | Commercial (Bastionsoft EULA)                         |\n| Weights   | Open on HuggingFace             | Gated — granted on purchase                           |\n\nThe commercial model extends coverage to seven languages at a lower\nfalse-positive rate. Request a quote at <https://bastionsoft.com>.\n\nThe commercial weights are gated on HuggingFace, so downloading them needs a\ntoken from an account granted access — set `$HF_TOKEN` or pass `hfToken`. The\nfree `tiny` model is public and needs no token.\n\n```ts\nconst guard = new Guard({ preset: \"multilingual\", requireLicense: true });\nguard.licenseStatus();\n// { valid: true, reason: \"valid\", licenseId: \"…\", tier: \"enterprise\",\n//   company: \"…\", validUntil: \"…\", expired: false }\n```\n\nLicenses are verified offline via Ed25519 — no network call, so it works\nair-gapped.\n\n## Scope\n\nThis release covers the detection engine. Framework integrations\n(LangChain, LlamaIndex, LiteLLM) are available in the Python package and are not\nyet here.\n\nFor a language-agnostic HTTP service, we publish\n[Docker images](https://github.com/bastion-soft/bastion-prompt-protection/tree/main/docker)\nexposing the same detector over `POST /protect`.\n\n## Examples\n\nRunnable tutorials in [`examples/`](examples/README.md) — raw ONNX with no SDK,\nthe standard integration, and offline/air-gapped operation.\n\n## Development\n\n```bash\nnpm install\nnpm run typecheck\nnpm run lint\nnpm test             # unit suite, no model download\nnpm run test:parity  # downloads weights, runs the fixture suite\nnpm run build\n```\n\nDetection behaviour is pinned by committed fixtures; see\n[`scripts/`](scripts/) for how they are regenerated, and\n[docs/measurements.md](docs/measurements.md) for the data behind the defaults.\n\nRelease process and npm/OIDC setup: [RELEASING.md](RELEASING.md).\n\n## Security\n\nDetection bypasses and package vulnerabilities: see [SECURITY.md](SECURITY.md).\nReleases are published from CI via npm Trusted Publishing (OIDC) with provenance\nattached — verify with `npm audit signatures`.\n\n## License\n\nAGPL-3.0-or-later. Commercial licensing: <https://bastionsoft.com>\n","readmeFilename":"README.md"}