@privacyscrubber/sdk 2.2.10 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![Air-Gapped](https://img.shields.io/badge/network_calls-0-blueviolet.svg)](https://privacyscrubber.com)
7
7
  [![TypeScript Typings](https://img.shields.io/badge/types-TypeScript-blue.svg)](https://www.npmjs.com/package/@privacyscrubber/sdk)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
- [![Patent Pending](https://img.shields.io/badge/Patent-Pending%20(ILPO%20331905)-06b6d4.svg)](https://privacyscrubber.com/patents/)
9
+ [![Patent Pending](https://img.shields.io/badge/Patent-Pending%20(US%20%26%20Intl)-06b6d4.svg)](https://privacyscrubber.com/patents/)
10
10
  [![IETF Specification](https://img.shields.io/badge/IETF-ZTDS--Protocol-0284c7.svg)](https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/)
11
11
  [![ZTDS Verified](https://ztds.ai/badge/privacyscrubber-sdk.svg)](https://ztds.ai/registry/#privacyscrubber-sdk)
12
12
  [![GitHub Stars](https://img.shields.io/github/stars/moxno/privacyscrubber-mcp?style=social)](https://github.com/moxno/privacyscrubber-mcp)
@@ -71,7 +71,7 @@ Backend engineering and InfoSec review boards consistently ask four core archite
71
71
  > - **CERN / Zenodo Foundation**: [DOI 10.5281/zenodo.22058770](https://doi.org/10.5281/zenodo.22058770)
72
72
  > - **Center for Open Science (OSF) Benchmark**: [DOI 10.17605/OSF.IO/5BYJF](https://doi.org/10.17605/OSF.IO/5BYJF)
73
73
  > - **Law Archive Treatise**: [osf.io/preprints/lawarchive/4wc86/](https://osf.io/preprints/lawarchive/4wc86/)
74
- > - **Patent Pending**: Israel Patent Office Application `IL 331905` (WIPO DAS Code: `B17B`)
74
+ > - **Patent Pending**: U.S. & International Patents Pending (Paris Convention / PCT Priority)
75
75
 
76
76
  ---
77
77
 
@@ -700,16 +700,15 @@ MIT License © 2026 PrivacyScrubber. Commercial production usage without quota l
700
700
 
701
701
  ---
702
702
 
703
- ## 🏛️ Intellectual Property & Virtual Patent Marking
703
+ ## 🏛️ Intellectual Property & Patent Disclosures
704
704
 
705
705
  The Zero-Trust Data Sanitization (ZTDS) architecture, in-memory deterministic tokenization, cryptographic session handoff, and headless execution methods implemented in this SDK are proprietary technology of Ilya Sibiryakov (BrandMeWeb) and are protected under **Patent Pending** status:
706
706
 
707
- - **Patent Office:** State of Israel Ministry of Justice, Patent Office (ILPO)
708
- - **Application Number:** `331905` (Tracking ID: `94221`)
709
- - **WIPO DAS Access Control Code:** `B17B` (World Intellectual Property Organization Digital Access Service electronic priority document exchange for USPTO, EPO, CNIPA, and PCT receiving offices)
710
- - **Filing / Priority Date:** September 14, 2026 (Paris Convention Art. 4 & 35 U.S.C. § 119 Priority locked through September 14, 2027)
707
+ - **Legal Status:** U.S. & International Patents Pending
708
+ - **Priority Framework:** Paris Convention Art. 4 & 35 U.S.C. § 119 Priority (locked through September 14, 2027)
709
+ - **Filing / Priority Date:** September 14, 2026
711
710
  - **Official Title:** *SYSTEM AND METHOD FOR CLIENT-SIDE ZERO-TRUST DATA SANITIZATION AND CRYPTOGRAPHIC SESSION HANDOFF IN ARTIFICIAL INTELLIGENCE WORKFLOWS*
712
- - **Virtual Patent Marking:** [privacyscrubber.com/patents/](https://privacyscrubber.com/patents/) in accordance with 35 U.S.C. § 287(a).
711
+ - **Patent Disclosures Hub:** [privacyscrubber.com/patents/](https://privacyscrubber.com/patents/) (formal verification via counsel NDA).
713
712
 
714
713
  ### 🌐 Internet Engineering Task Force (IETF) Specification
715
714
  - **Specification Title:** *The Zero-Trust Data Sanitization (ZTDS) Protocol for Frontier Artificial Intelligence Ingestion*
package/browser.js CHANGED
@@ -125,29 +125,77 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
125
125
  frameworksSet.add('GDPR (Art. 4)');
126
126
  frameworksSet.add('CCPA/CPRA');
127
127
  }
128
- if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT')) {
128
+ if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
129
129
  frameworksSet.add('SOC 2 Type II');
130
130
  frameworksSet.add('ISO 27001 (A.8.11)');
131
131
  }
132
- if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY'].includes(t))) {
132
+ if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
133
133
  frameworksSet.add('PCI DSS v4.0');
134
+ frameworksSet.add('IRC § 7216 & GLBA');
134
135
  }
135
- if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT'].includes(t))) {
136
+ if (types.some(t => ['MRN', 'HEALTH', 'MEDICAL', 'PATIENT', 'DIAGNOSIS', 'NPI', 'DEA'].includes(t))) {
136
137
  frameworksSet.add('HIPAA §164.514');
137
138
  }
138
- if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY', 'AWS_KEY', 'JWT_TOKEN', 'API_TOKEN'].includes(t))) {
139
+ if (types.some(t => ['CASE_NUMBER', 'DOCKET', 'PRIVILEGED', 'LEGAL', 'ATTORNEY', 'COURT', 'CONTRACT', 'NDA'].includes(t))) {
140
+ frameworksSet.add('FRE 502 (Privilege)');
141
+ }
142
+ if (types.some(t => ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'KEY', 'AWS_KEY', 'JWT_TOKEN', 'API_TOKEN', 'CREDENTIAL'].includes(t))) {
139
143
  frameworksSet.add('NIST SP 800-53');
140
144
  }
145
+ if (totalCount > 0) {
146
+ frameworksSet.add('EU AI Act (Art. 50)');
147
+ }
148
+
149
+ const ipHoldings = {
150
+ standard: 'IETF draft-sibiryakov-ztds-protocol-00',
151
+ standardUrl: 'https://datatracker.ietf.org/doc/draft-sibiryakov-ztds-protocol/',
152
+ patent: 'IL 331905 (Tracking: 94221)',
153
+ wipoDas: 'B17B',
154
+ trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
155
+ };
156
+
157
+ const statutoryCitations = [
158
+ {
159
+ statute: 'EU AI Act (Art. 50)',
160
+ scope: 'AI Transparency & Data Minimization',
161
+ rule: '0-byte external network transmission; prevents unauthorized ingestion into frontier models.'
162
+ },
163
+ {
164
+ statute: 'GDPR (Art. 4, 25, 28, 32)',
165
+ scope: 'Pseudonymization & Processor Liability',
166
+ rule: 'Volatile client-side tokenization eliminates GDPR Article 28 data processor liability.'
167
+ },
168
+ {
169
+ statute: 'HIPAA 45 CFR § 164.514(b)',
170
+ scope: 'Safe Harbor De-Identification',
171
+ rule: 'Deterministic removal of all 18 PHI identifiers prior to AI analysis.'
172
+ },
173
+ {
174
+ statute: 'IRC § 7216 & IRS Pub. 1075',
175
+ scope: 'Tax Return Information Confidentiality',
176
+ rule: 'Strict criminal penalty safeguard against unauthorized disclosure of tax records to cloud LLMs.'
177
+ },
178
+ {
179
+ statute: 'FRE 502',
180
+ scope: 'Attorney-Client Privilege Preservation',
181
+ rule: 'Automated in-memory redaction prevents inadvertent waiver of legal privilege in AI workflows.'
182
+ }
183
+ ];
141
184
 
142
185
  return {
143
186
  totalCount,
144
187
  entities,
145
188
  types,
146
189
  riskLevel,
147
- frameworksList: Array.from(frameworksSet)
190
+ frameworksList: Array.from(frameworksSet),
191
+ statutoryCitations,
192
+ ipHoldings,
193
+ multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
148
194
  };
149
195
  }
150
196
 
197
+ export const buildCisoAuditTelemetry = buildAuditTelemetry;
198
+
151
199
  export function formatAuditReceipt(telemetry) {
152
200
  if (!telemetry || telemetry.totalCount === 0) {
153
201
  return "> 🛡️ **PrivacyScrubber Audit Receipt**: CLEAN (ZERO PII DETECTED)\n" +
@@ -501,84 +549,230 @@ export class PrivacyScrubberEngine {
501
549
  }
502
550
 
503
551
  /**
504
- * Browser & Web Worker Streaming Detokenizer.
552
+ * Helper to escape special regex characters.
505
553
  */
506
- export class StreamingDetokenizer {
507
- constructor(tokenMap = {}, options = {}) {
508
- this.tokenMap = { ...tokenMap };
509
- this.buffer = '';
510
- this.onTokenRestored = typeof options.onTokenRestored === 'function' ? options.onTokenRestored : null;
511
- this.maxTokenLength = Math.max(
512
- 32,
513
- ...Object.keys(this.tokenMap).map(k => k.length + 8)
514
- );
515
- }
554
+ function escapeRegex(s) {
555
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
556
+ }
516
557
 
517
- updateTokenMap(newTokenMap) {
518
- Object.assign(this.tokenMap, newTokenMap);
519
- this.maxTokenLength = Math.max(
520
- this.maxTokenLength,
521
- ...Object.keys(this.tokenMap).map(k => k.length + 8)
522
- );
558
+ /**
559
+ * Builds an ultra-fast streaming token replacement function.
560
+ * Preserves all whitespace, indentation, newlines, and code blocks (does NOT trim).
561
+ * Handles descending key ordering (e.g. [NAME_10] before [NAME_1]), case insensitivity,
562
+ * alternative delimiters (<...>, {{...}}, __...__), and possessive suffixes.
563
+ *
564
+ * @param {Record<string, string>} tokenMap
565
+ * @param {Object} [options]
566
+ * @returns {(text: string) => string}
567
+ */
568
+ export function buildStreamReplacer(tokenMap = {}, options = {}) {
569
+ if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
570
+ return (text) => text || '';
571
+ }
572
+
573
+ const onTokenRestored = typeof options?.onTokenRestored === 'function' ? options.onTokenRestored : null;
574
+ const lookup = new Map();
575
+ const patterns = [];
576
+ const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
577
+
578
+ for (const k of sortedKeys) {
579
+ const val = tokenMap[k];
580
+ const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
581
+ const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
582
+
583
+ if (match) {
584
+ const label = escapeRegex(match[1]);
585
+ const idx = match[2];
586
+ const patternStr = '(?:\\[\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\]|<\\s*' + label + '[-_\\s]*0*' + idx + '\\s*>|\\{\\{\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\}\\}|__\\s*' + label + '[-_\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
587
+ patterns.push(patternStr);
588
+ const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
589
+ lookup.set(normKey, val);
590
+ lookup.set(k, val);
591
+ lookup.set(k.toUpperCase(), val);
592
+ } else {
593
+ const safe = escapeRegex(k);
594
+ patterns.push(safe);
595
+ lookup.set(k, val);
596
+ lookup.set(k.toUpperCase(), val);
597
+ }
523
598
  }
524
599
 
525
- process(chunk) {
526
- if (!chunk) return '';
527
- this.buffer += chunk;
528
- let output = '';
529
-
530
- while (true) {
531
- const openIdx = this.buffer.search(/\[|\<|\{\{|\_\_/);
532
- if (openIdx === -1) {
533
- output += this.buffer;
534
- this.buffer = '';
535
- break;
536
- }
600
+ const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
537
601
 
538
- output += this.buffer.slice(0, openIdx);
539
- this.buffer = this.buffer.slice(openIdx);
602
+ return function replaceTokens(text) {
603
+ if (!text) return '';
604
+ unifiedRegex.lastIndex = 0;
605
+ return text.replace(unifiedRegex, (match, suffix) => {
606
+ const isPossessive = suffix && /('s|’s)$/i.test(suffix);
607
+ const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
608
+ let restored = null;
540
609
 
541
- const closeIdx = this.buffer.search(/\]|\>|\}\}|\_\_/);
542
- if (closeIdx === -1) {
543
- if (this.buffer.length > this.maxTokenLength) {
544
- output += this.buffer.slice(0, this.maxTokenLength);
545
- this.buffer = this.buffer.slice(this.maxTokenLength);
546
- continue;
610
+ if (lookup.has(coreMatch)) {
611
+ restored = lookup.get(coreMatch);
612
+ } else if (lookup.has(coreMatch.toUpperCase())) {
613
+ restored = lookup.get(coreMatch.toUpperCase());
614
+ } else {
615
+ const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
616
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
617
+ if (m) {
618
+ const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
619
+ if (lookup.has(normKey)) {
620
+ restored = lookup.get(normKey);
621
+ }
547
622
  }
548
- break;
549
623
  }
550
624
 
551
- const candidateToken = this.buffer.slice(0, closeIdx + 1);
552
- const originalValue = this.tokenMap[candidateToken];
625
+ if (restored !== null) {
626
+ if (onTokenRestored) {
627
+ try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
628
+ }
629
+ return restored + (isPossessive ? "'s" : '');
630
+ }
553
631
 
554
- if (originalValue !== undefined) {
555
- output += originalValue;
556
- if (this.onTokenRestored) {
557
- this.onTokenRestored({ token: candidateToken, originalValue });
632
+ if (lookup.has(match)) {
633
+ restored = lookup.get(match);
634
+ if (onTokenRestored) {
635
+ try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
558
636
  }
559
- this.buffer = this.buffer.slice(closeIdx + 1);
560
- } else {
561
- output += candidateToken[0];
562
- this.buffer = this.buffer.slice(1);
637
+ return restored;
563
638
  }
639
+
640
+ return match;
641
+ });
642
+ };
643
+ }
644
+
645
+ /**
646
+ * Browser & Web Worker Streaming Detokenizer with sliding window buffer for LLM chunk boundary splits.
647
+ * Seamlessly restores split tokens (e.g. [EM + AIL_1]), preserves multi-byte UTF-8,
648
+ * and maintains zero-latency streaming throughput.
649
+ */
650
+ export class StreamingDetokenizer {
651
+ constructor(tokenMap = {}, options = {}) {
652
+ this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
653
+ this.options = options || {};
654
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
655
+ this.pendingBuffer = '';
656
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
657
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
658
+ this.decoder = new TextDecoder('utf-8', { fatal: false });
659
+ }
660
+
661
+ /**
662
+ * Backward-compatible buffer property getter/setter.
663
+ */
664
+ get buffer() {
665
+ return this.pendingBuffer;
666
+ }
667
+
668
+ set buffer(val) {
669
+ this.pendingBuffer = typeof val === 'string' ? val : '';
670
+ }
671
+
672
+ /**
673
+ * Dynamically updates the active token map for this streaming session.
674
+ * @param {Record<string, string>} newMap
675
+ */
676
+ updateTokenMap(newMap) {
677
+ if (newMap && typeof newMap === 'object') {
678
+ this.tokenMap = newMap;
679
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
680
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
681
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Resets the internal sliding window buffer.
687
+ */
688
+ reset() {
689
+ this.pendingBuffer = '';
690
+ }
691
+
692
+ /**
693
+ * Feeds an incoming chunk (string, Uint8Array, or Buffer) and returns de-tokenized text.
694
+ * Buffers any unclosed token candidates at chunk boundaries until closed or flushed.
695
+ * @param {string | Uint8Array | Buffer} chunk
696
+ * @returns {string}
697
+ */
698
+ feed(chunk) {
699
+ if (chunk === undefined || chunk === null) return '';
700
+ let text = '';
701
+ if (typeof chunk === 'string') {
702
+ text = chunk;
703
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
704
+ text = this.decoder.decode(chunk, { stream: true });
705
+ } else {
706
+ text = String(chunk);
707
+ }
708
+
709
+ if (!text) return '';
710
+
711
+ // Dynamically recompile if tokens were added to the referenced map
712
+ const currentTokenCount = Object.keys(this.tokenMap).length;
713
+ if (currentTokenCount !== this._lastTokenCount) {
714
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
715
+ this.hasTokens = currentTokenCount > 0;
716
+ this._lastTokenCount = currentTokenCount;
564
717
  }
565
718
 
566
- return output;
719
+ if (!this.hasTokens) return text;
720
+
721
+ this.pendingBuffer += text;
722
+
723
+ // Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
724
+ // Matches incomplete tokens starting with [, <, {{, or __, plus single delimiter prefixes { and _
725
+ const tailLen = 160;
726
+ const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
727
+ const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
728
+ const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-А-Яа-яЁё ]{0,128}$/);
729
+
730
+ if (m) {
731
+ const candidate = m[0];
732
+ // Only closed double-underscore tokens (e.g. __NAME_1__) could match m since '_' is in character class.
733
+ // Bracketed tokens ([, <, {{) cannot end with their closing delimiter here and must be buffered.
734
+ const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
735
+ if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
736
+ const toEmit = this.pendingBuffer;
737
+ this.pendingBuffer = '';
738
+ return this.replacer(toEmit);
739
+ }
740
+ const splitIdx = offset + m.index;
741
+ const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
742
+ this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
743
+ return safeToEmit ? this.replacer(safeToEmit) : '';
744
+ }
745
+
746
+ const toEmit = this.pendingBuffer;
747
+ this.pendingBuffer = '';
748
+ return this.replacer(toEmit);
567
749
  }
568
750
 
751
+ /**
752
+ * Alias for feed() to maintain backwards compatibility with process() callers.
753
+ */
754
+ process(chunk) {
755
+ return this.feed(chunk);
756
+ }
757
+
758
+ /**
759
+ * Flushes remaining buffered tokens and finalizes UTF-8 stream decoder.
760
+ * @returns {string}
761
+ */
569
762
  flush() {
570
- let output = '';
571
- if (this.buffer) {
572
- let flushed = this.buffer;
573
- for (const [token, original] of Object.entries(this.tokenMap)) {
574
- if (flushed.includes(token)) {
575
- flushed = flushed.split(token).join(original);
576
- }
577
- }
578
- output = flushed;
579
- this.buffer = '';
763
+ let text = '';
764
+ try {
765
+ text = this.decoder.decode(new Uint8Array(), { stream: false });
766
+ } catch (_) {}
767
+
768
+ if (text) {
769
+ this.pendingBuffer += text;
580
770
  }
581
- return output;
771
+
772
+ if (!this.pendingBuffer) return '';
773
+ const remaining = this.pendingBuffer;
774
+ this.pendingBuffer = '';
775
+ return this.replacer(remaining);
582
776
  }
583
777
  }
584
778
 
@@ -634,11 +828,6 @@ export function createStreamTokenRestorer(tokenMap = {}, options = {}) {
634
828
  };
635
829
  }
636
830
 
637
- export function buildStreamReplacer(tokenMap = {}) {
638
- const detok = new StreamingDetokenizer(tokenMap);
639
- return (chunk) => detok.process(chunk);
640
- }
641
-
642
831
  export const scrubText = scrubberPkg ? scrubberPkg.scrubText : null;
643
832
  export const unscrubText = scrubberPkg ? scrubberPkg.unscrubText : null;
644
833
 
@@ -665,6 +854,7 @@ export default {
665
854
  validateLicense,
666
855
  validateLicenseKey,
667
856
  buildAuditTelemetry,
857
+ buildCisoAuditTelemetry,
668
858
  formatAuditReceipt,
669
859
  PrivacyScrubberEngine,
670
860
  PrivacyScrubberLicenseError,
package/cli.js CHANGED
@@ -14,7 +14,7 @@ import path from 'node:path';
14
14
  import crypto from 'node:crypto';
15
15
  import http from 'node:http';
16
16
  import { fileURLToPath } from 'node:url';
17
- import { sanitize, restore, generateProcurementMemo, StreamingDetokenizer } from './index.js';
17
+ import { sanitize, restore, validateLicense, generateProcurementMemo, StreamingDetokenizer } from './index.js';
18
18
 
19
19
  const __filename = fileURLToPath(import.meta.url);
20
20
  const __dirname = path.dirname(__filename);
@@ -61,17 +61,24 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
61
61
  if (portIdx !== -1 && args[portIdx + 1]) {
62
62
  port = parseInt(args[portIdx + 1], 10);
63
63
  }
64
+ let host = process.env.HOST || '127.0.0.1';
65
+ const hostIdx = args.indexOf('--host');
66
+ if (hostIdx !== -1 && args[hostIdx + 1]) {
67
+ host = args[hostIdx + 1];
68
+ }
64
69
  let socketPath = null;
65
70
  const sockIdx = args.indexOf('--socket');
66
71
  if (sockIdx !== -1 && args[sockIdx + 1]) {
67
72
  socketPath = args[sockIdx + 1];
68
73
  }
69
74
 
75
+ const defaultEnvKey = process.env.PRIVACYSCRUBBER_KEY || process.env.PRIVACYSCRUBBER_LICENSE_KEY || process.env.ZTDS_LICENSE_KEY || '';
76
+
70
77
  const server = http.createServer((req, res) => {
71
- // CORS headers for local loopback
78
+ // CORS headers for local loopback & container IPC
72
79
  res.setHeader('Access-Control-Allow-Origin', '*');
73
80
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
74
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
81
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-License-Key');
75
82
 
76
83
  if (req.method === 'OPTIONS') {
77
84
  res.writeHead(204);
@@ -79,9 +86,46 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
79
86
  return;
80
87
  }
81
88
 
82
- if (req.method === 'GET' && (req.url === '/' || req.url === '/health')) {
89
+ const reqUrl = req.url ? req.url.split('?')[0] : '/';
90
+
91
+ // Extract authorization key from header if present
92
+ let headerKey = '';
93
+ const authHeader = req.headers['authorization'] || '';
94
+ if (authHeader.startsWith('Bearer ')) {
95
+ headerKey = authHeader.slice(7).trim();
96
+ } else if (authHeader) {
97
+ headerKey = authHeader.trim();
98
+ }
99
+ const xKey = req.headers['x-license-key'] ? String(req.headers['x-license-key']).trim() : '';
100
+ const effectiveServerKey = headerKey || xKey || defaultEnvKey;
101
+
102
+ if (req.method === 'GET' && (reqUrl === '/' || reqUrl === '/health' || reqUrl === '/v1/health')) {
103
+ const lic = validateLicense(effectiveServerKey);
83
104
  res.writeHead(200, { 'Content-Type': 'application/json' });
84
- res.end(JSON.stringify({ status: 'ok', engine: 'ZTDS', tier: 'Developer SDK', version: '2.2.6' }));
105
+ res.end(JSON.stringify({
106
+ status: 'ok',
107
+ engine: 'ZTDS',
108
+ tier: lic.valid ? (lic.tier || 'SDK') : 'Community (Free Tier)',
109
+ licensed: !!lic.valid,
110
+ version: '2.2.6',
111
+ networkEgress: '0 bytes (100% in-memory RAM)',
112
+ uptimeSec: Math.floor(process.uptime()),
113
+ memoryMb: (process.memoryUsage().heapUsed / (1024 * 1024)).toFixed(2)
114
+ }));
115
+ return;
116
+ }
117
+
118
+ if (req.method === 'GET' && (reqUrl === '/metrics' || reqUrl === '/v1/metrics')) {
119
+ const mem = process.memoryUsage();
120
+ res.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4' });
121
+ res.end(
122
+ `# HELP ztds_uptime_seconds Process uptime\n` +
123
+ `# TYPE ztds_uptime_seconds counter\n` +
124
+ `ztds_uptime_seconds ${Math.floor(process.uptime())}\n` +
125
+ `# HELP ztds_memory_heap_bytes Process heap usage\n` +
126
+ `# TYPE ztds_memory_heap_bytes gauge\n` +
127
+ `ztds_memory_heap_bytes ${mem.heapUsed}\n`
128
+ );
85
129
  return;
86
130
  }
87
131
 
@@ -99,9 +143,16 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
99
143
  req.on('end', () => {
100
144
  try {
101
145
  const payload = bodyStr ? JSON.parse(bodyStr) : {};
102
- if (req.url === '/scrub' || req.url === '/sanitize') {
146
+ const isScrub = reqUrl === '/scrub' || reqUrl === '/sanitize' || reqUrl === '/v1/scrub' || reqUrl === '/v1/sanitize';
147
+ const isUnscrub = reqUrl === '/unscrub' || reqUrl === '/restore' || reqUrl === '/v1/unscrub' || reqUrl === '/v1/restore';
148
+
149
+ if (isScrub) {
103
150
  const text = payload.text || '';
104
151
  const opts = payload.options || { profile: payload.profile || 'General', detectSecrets: true };
152
+ if (!opts.licenseKey) {
153
+ opts.licenseKey = payload.licenseKey || effectiveServerKey;
154
+ }
155
+
105
156
  const start = process.hrtime.bigint();
106
157
  const result = sanitize(text, opts);
107
158
  const end = process.hrtime.bigint();
@@ -111,6 +162,7 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
111
162
  res.end(JSON.stringify({
112
163
  sanitizedText: result.scrubbedText,
113
164
  tokenMap: result.tokenMap,
165
+ license: result.license,
114
166
  telemetry: {
115
167
  ...result.telemetry,
116
168
  latencyUs: durationUs,
@@ -120,7 +172,7 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
120
172
  return;
121
173
  }
122
174
 
123
- if (req.url === '/unscrub' || req.url === '/restore') {
175
+ if (isUnscrub) {
124
176
  const text = payload.text || '';
125
177
  const tokenMap = payload.tokenMap || {};
126
178
  const { restoredText, restoredCount } = restore(text, tokenMap);
@@ -130,10 +182,10 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
130
182
  }
131
183
 
132
184
  res.writeHead(404, { 'Content-Type': 'application/json' });
133
- res.end(JSON.stringify({ error: 'Endpoint Not Found. Supported: /scrub, /unscrub, /health' }));
185
+ res.end(JSON.stringify({ error: 'Endpoint Not Found. Supported: /v1/scrub, /v1/unscrub, /v1/health, /v1/metrics' }));
134
186
  } catch (err) {
135
187
  res.writeHead(400, { 'Content-Type': 'application/json' });
136
- res.end(JSON.stringify({ error: 'Invalid JSON payload', message: err.message }));
188
+ res.end(JSON.stringify({ error: 'Invalid JSON payload or processing error', message: err.message }));
137
189
  }
138
190
  });
139
191
  return;
@@ -152,9 +204,9 @@ if (args.includes('sidecar') || args.includes('--sidecar') || args.includes('ser
152
204
  console.log(`[ZTDS Sidecar] Ready for Python/Go local IPC with <1ms in-memory execution.`);
153
205
  });
154
206
  } else {
155
- server.listen(port, '127.0.0.1', () => {
156
- console.log(`[ZTDS Sidecar] HTTP Daemon listening on http://127.0.0.1:${port}`);
157
- console.log(`[ZTDS Sidecar] Endpoints: POST /scrub | POST /unscrub | GET /health`);
207
+ server.listen(port, host, () => {
208
+ console.log(`[ZTDS Sidecar] HTTP Daemon listening on http://${host}:${port}`);
209
+ console.log(`[ZTDS Sidecar] Endpoints: POST /v1/scrub | POST /v1/unscrub | GET /v1/health | GET /v1/metrics`);
158
210
  console.log(`[ZTDS Sidecar] Latency: <1.2ms (Zero network hops, local loopback)`);
159
211
  });
160
212
  }
package/index.d.ts CHANGED
@@ -26,6 +26,8 @@ export type PiiProfile =
26
26
  | 'Compliance'
27
27
  | 'CCPA'
28
28
  | 'Engineering'
29
+ | 'Manufacturing'
30
+ | 'Industrial'
29
31
  | 'Agents'
30
32
  | 'Academic'
31
33
  | 'Creative'
@@ -103,12 +105,28 @@ export interface LicenseMetadata {
103
105
  recommendation?: string;
104
106
  }
105
107
 
108
+ export interface StatutoryCitation {
109
+ statute: string;
110
+ scope: string;
111
+ rule: string;
112
+ }
113
+
114
+ export interface IpHoldings {
115
+ standard: string;
116
+ standardUrl: string;
117
+ patent: string;
118
+ wipoDas: string;
119
+ trademark: string;
120
+ }
121
+
106
122
  export interface AuditTelemetry {
107
123
  totalCount: number;
108
124
  entities: Record<string, number>;
109
125
  types: string[];
110
126
  riskLevel: 'CLEAN (ZERO PII)' | 'LOW EXPOSURE' | 'MODERATE EXPOSURE' | 'CRITICAL (HIGH EXPOSURE)';
111
127
  frameworksList: string[];
128
+ statutoryCitations?: StatutoryCitation[];
129
+ ipHoldings?: IpHoldings;
112
130
  }
113
131
 
114
132
  export interface SanitizeResult {
@@ -160,6 +178,7 @@ export function validateLicense(key: string): LicenseValidationResult;
160
178
  export function validateLicenseKey(key: string): 'PRO' | 'TEAMS' | 'SDK' | 'ENTERPRISE' | 'OEM' | null;
161
179
 
162
180
  export function buildAuditTelemetry(currentTokenMap?: Record<string, string> | string[]): AuditTelemetry;
181
+ export const buildCisoAuditTelemetry: typeof buildAuditTelemetry;
163
182
  export function formatAuditReceipt(telemetry: AuditTelemetry): string;
164
183
 
165
184
  export interface AuditEventData {