@privacyscrubber/sdk 2.2.4 → 2.2.6

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/index.js CHANGED
@@ -12,6 +12,7 @@ import { createRequire } from 'module';
12
12
  const require = createRequire(import.meta.url);
13
13
  import fs from 'fs';
14
14
  import path from 'path';
15
+ import os from 'os';
15
16
  import { execSync } from 'child_process';
16
17
  import { Transform } from 'stream';
17
18
 
@@ -223,7 +224,7 @@ function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
223
224
  process.stderr.write(
224
225
  `\n[PrivacyScrubber] Intercepted ${count} API secret(s) before LLM egress: ${secretSample}\n` +
225
226
  `[PrivacyScrubber] Protected in local RAM (0 byte network egress). Free Tier active (${limit.toLocaleString()} char cap).\n` +
226
- `[PrivacyScrubber] Developer SDK ($199/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n\n`
227
+ `[PrivacyScrubber] Developer SDK ($299/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n\n`
227
228
  );
228
229
  }
229
230
  }
@@ -288,13 +289,13 @@ Pre-configured for 30 specialized industry profiles and compliance frameworks:
288
289
  - Latency: +200ms - 400ms added network roundtrip per AI prompt.
289
290
  - Sub-processor Risk: Requires vendor DPA, external network egress.
290
291
  * PrivacyScrubber Developer SDK:
291
- - Cost: $199 / month flat (or $1,990 / year) for unlimited nodes, microservices, and prompts.
292
+ - Cost: $299 / month flat (or $2,990 / year) for unlimited nodes, microservices, and prompts.
292
293
  - Latency: <0.1ms (in-process RAM).
293
294
  - Sub-processor Risk: Zero (pure in-memory software library).
294
295
 
295
296
  5. ACTION REQUESTED
296
297
  Approve commercial license upgrade for ${developer} (${teamSize} engineers, unlimited backend nodes):
297
- * License Tier: Developer SDK ($199/month flat or $1,990/year)
298
+ * License Tier: Developer SDK ($299/month flat or $2,990/year)
298
299
  * Direct Procurement & License Activation: https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo
299
300
  * Enterprise / Invoicing Contact: enterprise@privacyscrubber.com
300
301
  `;
@@ -341,7 +342,7 @@ The SDK enforces out-of-the-box sanitization matching mandatory compliance contr
341
342
 
342
343
  | Evaluation Factor | Traditional Cloud DLP (AWS/GCP/Nightfall) | PrivacyScrubber Developer SDK |
343
344
  | :--- | :--- | :--- |
344
- | **Pricing Model** | \$1,500 – \$4,000 / month (metered per GB/token) | **\$199 / month flat** (unlimited nodes & tokens) |
345
+ | **Pricing Model** | \$1,500 – \$4,000 / month (metered per GB/token) | **\$299 / month flat** (unlimited nodes & tokens) |
345
346
  | **Network Egress** | Requires outbound network calls to DLP vendor | **0 bytes** (100% local Node.js process memory) |
346
347
  | **Added Latency** | +200ms – +450ms network round-trip | **<0.1ms** (in-memory regular expressions) |
347
348
  | **Vendor Sub-processors** | Third-party cloud vendor added to vendor list | **None** (zero-network software library) |
@@ -350,7 +351,7 @@ The SDK enforces out-of-the-box sanitization matching mandatory compliance contr
350
351
  ---
351
352
 
352
353
  ### 5. Procurement Recommendation
353
- We request approval to acquire a **Developer SDK License** (\$199/month flat or \$1,990/year) to unlock unlimited throughput across all microservices and CI/CD pipelines.
354
+ We request approval to acquire a **Developer SDK License** (\$299/month flat or \$2,990/year) to unlock unlimited throughput across all microservices and CI/CD pipelines.
354
355
 
355
356
  * **Direct Procurement Portal:** [https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo](https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo)
356
357
  * **Procurement / Invoicing Contact:** \`enterprise@privacyscrubber.com\`
@@ -821,6 +822,16 @@ export function wrapOpenAI(openaiClient, options = {}) {
821
822
  const res = engine.sanitize(msg.content, { existingSessionMap: sessionTokenMap });
822
823
  Object.assign(sessionTokenMap, res.tokenMap);
823
824
  return { ...msg, content: res.scrubbedText };
825
+ } else if (Array.isArray(msg.content)) {
826
+ const sanitizedChunks = msg.content.map(chunk => {
827
+ if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
828
+ const res = engine.sanitize(chunk.text, { existingSessionMap: sessionTokenMap });
829
+ Object.assign(sessionTokenMap, res.tokenMap);
830
+ return { ...chunk, text: res.scrubbedText };
831
+ }
832
+ return chunk;
833
+ });
834
+ return { ...msg, content: sanitizedChunks };
824
835
  }
825
836
  return msg;
826
837
  });
@@ -836,8 +847,17 @@ export function wrapOpenAI(openaiClient, options = {}) {
836
847
 
837
848
  if (response && Array.isArray(response.choices)) {
838
849
  response.choices.forEach(choice => {
839
- if (choice.message && typeof choice.message.content === 'string') {
840
- choice.message.content = engine.restore(choice.message.content, sessionTokenMap).restoredText;
850
+ if (choice.message) {
851
+ if (typeof choice.message.content === 'string') {
852
+ choice.message.content = engine.restore(choice.message.content, sessionTokenMap).restoredText;
853
+ } else if (Array.isArray(choice.message.content)) {
854
+ choice.message.content = choice.message.content.map(chunk => {
855
+ if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
856
+ return { ...chunk, text: engine.restore(chunk.text, sessionTokenMap).restoredText };
857
+ }
858
+ return chunk;
859
+ });
860
+ }
841
861
  }
842
862
  });
843
863
  }
@@ -859,6 +879,364 @@ export function createLangChainTransform(options = {}) {
859
879
  };
860
880
  }
861
881
 
882
+ /**
883
+ * LangChain Document Transformer for RAG pre-ingestion pipelines.
884
+ * Compliant with LangChain BaseDocumentTransformer and Runnable interfaces.
885
+ *
886
+ * Intercepts LangChain Document objects ({ pageContent, metadata }) before
887
+ * embedding or vector storage, scrubbing PII in pageContent and optional sensitive metadata keys.
888
+ *
889
+ * @param {Object} [options={}]
890
+ * @param {string} [options.profile='General'] Detection profile ('General', 'Medical', 'Finance', 'Dev', etc.)
891
+ * @param {string[]} [options.sensitiveMetadataKeys=[]] Metadata keys to sanitize (e.g. ['author', 'user_id', 'contact'])
892
+ * @param {boolean} [options.attachTelemetry=true] Whether to attach _ztds_telemetry to document metadata
893
+ * @param {boolean} [options.attachTokenMap=false] Whether to attach _ztds_token_map to document metadata
894
+ * @returns {Object} Document transformer with transformDocuments and invoke methods
895
+ */
896
+ export function createDocumentTransformer(options = {}) {
897
+ const {
898
+ profile = 'General',
899
+ sensitiveMetadataKeys = [],
900
+ attachTelemetry = true,
901
+ attachTokenMap = false,
902
+ ...engineOptions
903
+ } = options;
904
+
905
+ const engine = new PrivacyScrubberEngine({ profile, ...engineOptions });
906
+
907
+ async function transformDocuments(documents) {
908
+ if (!Array.isArray(documents)) {
909
+ throw new TypeError("documents must be an array of LangChain Document objects");
910
+ }
911
+
912
+ const transformed = [];
913
+ for (const doc of documents) {
914
+ const pageContent = doc.pageContent || doc.text || "";
915
+ const metadata = { ...(doc.metadata || {}) };
916
+
917
+ // Sanitize page content
918
+ const scrubResult = engine.sanitize(pageContent, { profile });
919
+
920
+ // Sanitize designated sensitive metadata fields if present
921
+ if (Array.isArray(sensitiveMetadataKeys) && sensitiveMetadataKeys.length > 0) {
922
+ for (const key of sensitiveMetadataKeys) {
923
+ if (metadata[key] && typeof metadata[key] === 'string') {
924
+ const metaScrub = engine.sanitize(metadata[key], { profile });
925
+ metadata[key] = metaScrub.scrubbedText;
926
+ }
927
+ }
928
+ }
929
+
930
+ if (attachTelemetry) {
931
+ metadata._ztds_telemetry = {
932
+ riskLevel: scrubResult.telemetry?.riskLevel || 'CLEAN (ZERO PII)',
933
+ totalCount: scrubResult.count || 0,
934
+ timestamp: new Date().toISOString()
935
+ };
936
+ }
937
+
938
+ if (attachTokenMap && scrubResult.tokenMap) {
939
+ metadata._ztds_token_map = { ...scrubResult.tokenMap };
940
+ }
941
+
942
+ transformed.push({
943
+ ...doc,
944
+ pageContent: scrubResult.scrubbedText,
945
+ metadata
946
+ });
947
+ }
948
+
949
+ return transformed;
950
+ }
951
+
952
+ return {
953
+ transformDocuments,
954
+ invoke: async (documents) => transformDocuments(documents),
955
+ sessionMap: engine.sessionMap,
956
+ restore: (output, tokenMap) => engine.restore(output, tokenMap || engine.sessionMap),
957
+ resetSession: () => engine.resetSession()
958
+ };
959
+ }
960
+ export const createLangChainDocumentTransformer = createDocumentTransformer;
961
+
962
+ /**
963
+ * LlamaIndex Node/Document Transform component for RAG ingestion.
964
+ *
965
+ * Compatible with LlamaIndex ingestion pipelines and custom transformations.
966
+ * Sanitizes node text while preserving node IDs, relationships, and metadata.
967
+ *
968
+ * @param {Object} [options={}]
969
+ * @returns {Object} LlamaIndex transform with transform method
970
+ */
971
+ export function createLlamaIndexTransform(options = {}) {
972
+ const { profile = 'General', ...engineOptions } = options;
973
+ const engine = new PrivacyScrubberEngine({ profile, ...engineOptions });
974
+
975
+ return {
976
+ transform: (nodes) => {
977
+ if (!nodes) return nodes;
978
+ const isArray = Array.isArray(nodes);
979
+ const list = isArray ? nodes : [nodes];
980
+
981
+ const transformed = list.map((node) => {
982
+ const text = typeof node.getContent === 'function'
983
+ ? node.getContent()
984
+ : (node.text || node.pageContent || '');
985
+ const result = engine.sanitize(text, { profile });
986
+
987
+ if (typeof node.setContent === 'function') {
988
+ node.setContent(result.scrubbedText);
989
+ } else {
990
+ node.text = result.scrubbedText;
991
+ }
992
+
993
+ if (node.metadata) {
994
+ node.metadata._ztds_sanitized = true;
995
+ node.metadata._ztds_pii_count = result.count;
996
+ }
997
+ return node;
998
+ });
999
+
1000
+ return isArray ? transformed : transformed[0];
1001
+ },
1002
+ sessionMap: engine.sessionMap,
1003
+ restore: (text, map) => engine.restore(text, map || engine.sessionMap),
1004
+ resetSession: () => engine.resetSession()
1005
+ };
1006
+ }
1007
+
1008
+ /**
1009
+ * Universal Vector Database Ingestion Guard (Chroma, Pinecone, Qdrant, Weaviate, pgvector, Milvus).
1010
+ *
1011
+ * Guarantees zero-PII vector embedding and storage in local/cloud vector databases.
1012
+ * Provides pre-ingestion record sanitization, query alignment, search result re-hydration,
1013
+ * and transparent proxy wrapping of vector store clients.
1014
+ *
1015
+ * @param {Object} [options={}]
1016
+ * @param {string} [options.profile='General']
1017
+ * @param {string[]} [options.sensitiveMetadataKeys=[]]
1018
+ * @param {boolean} [options.autoRestoreResults=true]
1019
+ * @returns {Object} Vector ingestion guard utilities
1020
+ */
1021
+ export function createVectorIngestionGuard(options = {}) {
1022
+ const {
1023
+ profile = 'General',
1024
+ sensitiveMetadataKeys = [],
1025
+ autoRestoreResults = true,
1026
+ ...engineOptions
1027
+ } = options;
1028
+
1029
+ const engine = new PrivacyScrubberEngine({ profile, ...engineOptions });
1030
+
1031
+ /**
1032
+ * Sanitizes raw database records prior to vector store upsert / add.
1033
+ * Supports standard formats:
1034
+ * - Chroma: { ids: [], documents: [], metadatas: [] }
1035
+ * - Pinecone: [{ id, values, metadata }]
1036
+ * - Qdrant: [{ id, vector, payload: { text, ... } }]
1037
+ * - Universal: [{ id, text, metadata }]
1038
+ */
1039
+ function sanitizeRecords(records) {
1040
+ if (!records) return records;
1041
+
1042
+ // Chroma-style columnar batch: { ids, documents, metadatas }
1043
+ if (records.documents && Array.isArray(records.documents)) {
1044
+ const sanitizedDocs = [];
1045
+ const sanitizedMetas = records.metadatas ? [...records.metadatas] : [];
1046
+ let totalTokens = 0;
1047
+
1048
+ records.documents.forEach((doc, idx) => {
1049
+ const res = engine.sanitize(doc || '', { profile });
1050
+ sanitizedDocs.push(res.scrubbedText);
1051
+ totalTokens += res.count;
1052
+
1053
+ if (sanitizedMetas[idx]) {
1054
+ const meta = { ...sanitizedMetas[idx] };
1055
+ if (sensitiveMetadataKeys.length > 0) {
1056
+ for (const k of sensitiveMetadataKeys) {
1057
+ if (typeof meta[k] === 'string') {
1058
+ meta[k] = engine.sanitize(meta[k], { profile }).scrubbedText;
1059
+ }
1060
+ }
1061
+ }
1062
+ meta._ztds_sanitized = true;
1063
+ sanitizedMetas[idx] = meta;
1064
+ }
1065
+ });
1066
+
1067
+ return {
1068
+ ...records,
1069
+ documents: sanitizedDocs,
1070
+ metadatas: sanitizedMetas,
1071
+ _ztds_total_tokens: totalTokens
1072
+ };
1073
+ }
1074
+
1075
+ // Array of record objects (Pinecone, Qdrant, Weaviate, LangChain vectorstores)
1076
+ if (Array.isArray(records)) {
1077
+ let totalTokens = 0;
1078
+ const sanitized = records.map((record) => {
1079
+ const copy = { ...record };
1080
+ // Handle text / document / pageContent / payload.text
1081
+ const textField = copy.text !== undefined ? 'text'
1082
+ : copy.document !== undefined ? 'document'
1083
+ : copy.pageContent !== undefined ? 'pageContent'
1084
+ : null;
1085
+
1086
+ if (textField && typeof copy[textField] === 'string') {
1087
+ const res = engine.sanitize(copy[textField], { profile });
1088
+ copy[textField] = res.scrubbedText;
1089
+ totalTokens += res.count;
1090
+ }
1091
+
1092
+ // Handle Qdrant payload
1093
+ if (copy.payload && typeof copy.payload === 'object') {
1094
+ copy.payload = { ...copy.payload };
1095
+ if (typeof copy.payload.text === 'string') {
1096
+ const res = engine.sanitize(copy.payload.text, { profile });
1097
+ copy.payload.text = res.scrubbedText;
1098
+ totalTokens += res.count;
1099
+ }
1100
+ }
1101
+
1102
+ // Handle metadata
1103
+ if (copy.metadata && typeof copy.metadata === 'object') {
1104
+ copy.metadata = { ...copy.metadata };
1105
+ for (const k of sensitiveMetadataKeys) {
1106
+ if (typeof copy.metadata[k] === 'string') {
1107
+ copy.metadata[k] = engine.sanitize(copy.metadata[k], { profile }).scrubbedText;
1108
+ }
1109
+ }
1110
+ copy.metadata._ztds_sanitized = true;
1111
+ }
1112
+
1113
+ return copy;
1114
+ });
1115
+
1116
+ return sanitized;
1117
+ }
1118
+
1119
+ return records;
1120
+ }
1121
+
1122
+ /**
1123
+ * Sanitizes user queries before generating search embeddings.
1124
+ * Ensures search vectors align with sanitized tokenized corpus representations.
1125
+ */
1126
+ function sanitizeQuery(query) {
1127
+ if (typeof query !== 'string') return query;
1128
+ const res = engine.sanitize(query, { profile });
1129
+ const matches = res.scrubbedText.match(/\[[A-Z_]+_\d+\]/g);
1130
+ const tokensDetected = matches ? matches.length : (res.count || 0);
1131
+ return {
1132
+ query: res.scrubbedText,
1133
+ rawQuery: query,
1134
+ tokensDetected,
1135
+ tokenMap: res.tokenMap
1136
+ };
1137
+ }
1138
+
1139
+ /**
1140
+ * Restores search results retrieved from vector DB using the volatile session map.
1141
+ */
1142
+ function restoreSearchResults(results, customTokenMap = null) {
1143
+ const map = customTokenMap || engine.sessionMap;
1144
+ if (!results || Object.keys(map).length === 0) return results;
1145
+
1146
+ function restoreText(t) {
1147
+ if (typeof t !== 'string') return t;
1148
+ return engine.restore(t, map).restoredText;
1149
+ }
1150
+
1151
+ if (Array.isArray(results)) {
1152
+ return results.map(item => {
1153
+ const copy = { ...item };
1154
+ if (typeof copy.pageContent === 'string') copy.pageContent = restoreText(copy.pageContent);
1155
+ if (typeof copy.text === 'string') copy.text = restoreText(copy.text);
1156
+ if (typeof copy.document === 'string') copy.document = restoreText(copy.document);
1157
+ if (copy.payload && typeof copy.payload === 'object') {
1158
+ copy.payload = { ...copy.payload };
1159
+ if (typeof copy.payload.text === 'string') {
1160
+ copy.payload.text = restoreText(copy.payload.text);
1161
+ }
1162
+ }
1163
+ return copy;
1164
+ });
1165
+ }
1166
+
1167
+ return results;
1168
+ }
1169
+
1170
+ /**
1171
+ * Wraps an existing vector store client instance with zero-trust hooks.
1172
+ */
1173
+ function wrapVectorStore(vectorStore) {
1174
+ const proxy = new Proxy(vectorStore, {
1175
+ get(target, prop, receiver) {
1176
+ const orig = Reflect.get(target, prop, receiver);
1177
+ if (typeof orig !== 'function') return orig;
1178
+
1179
+ // Intercept ingestion methods: addDocuments, upsert, add, insert
1180
+ if (prop === 'addDocuments') {
1181
+ return async function(docs, ...rest) {
1182
+ if (!Array.isArray(docs)) return orig.apply(target, [docs, ...rest]);
1183
+ const safeDocs = docs.map(doc => {
1184
+ const pageContent = doc.pageContent || doc.text || "";
1185
+ const metadata = { ...(doc.metadata || {}) };
1186
+ const scrubResult = engine.sanitize(pageContent, { profile });
1187
+ for (const key of sensitiveMetadataKeys) {
1188
+ if (metadata[key] && typeof metadata[key] === 'string') {
1189
+ metadata[key] = engine.sanitize(metadata[key], { profile }).scrubbedText;
1190
+ }
1191
+ }
1192
+ metadata._ztds_telemetry = {
1193
+ riskLevel: scrubResult.telemetry?.riskLevel || 'CLEAN (ZERO PII)',
1194
+ totalCount: scrubResult.count || 0,
1195
+ timestamp: new Date().toISOString()
1196
+ };
1197
+ return { ...doc, pageContent: scrubResult.scrubbedText, metadata };
1198
+ });
1199
+ return orig.apply(target, [safeDocs, ...rest]);
1200
+ };
1201
+ }
1202
+
1203
+ if (prop === 'upsert' || prop === 'add' || prop === 'insert') {
1204
+ return async function(records, ...rest) {
1205
+ const safeRecords = sanitizeRecords(records);
1206
+ return orig.apply(target, [safeRecords, ...rest]);
1207
+ };
1208
+ }
1209
+
1210
+ // Intercept query methods: similaritySearch, query, search
1211
+ if (prop === 'similaritySearch' || prop === 'query' || prop === 'search') {
1212
+ return async function(queryArg, ...rest) {
1213
+ const safeQ = typeof queryArg === 'string' ? sanitizeQuery(queryArg).query : queryArg;
1214
+ const rawResults = await orig.apply(target, [safeQ, ...rest]);
1215
+ if (autoRestoreResults) {
1216
+ return restoreSearchResults(rawResults);
1217
+ }
1218
+ return rawResults;
1219
+ };
1220
+ }
1221
+
1222
+ return orig.bind(target);
1223
+ }
1224
+ });
1225
+
1226
+ return proxy;
1227
+ }
1228
+
1229
+ return {
1230
+ sanitizeRecords,
1231
+ sanitizeQuery,
1232
+ restoreSearchResults,
1233
+ wrapVectorStore,
1234
+ sessionMap: engine.sessionMap,
1235
+ restore: (text, map) => engine.restore(text, map || engine.sessionMap),
1236
+ resetSession: () => engine.resetSession()
1237
+ };
1238
+ }
1239
+
862
1240
  /**
863
1241
  * Helper to escape special regex characters.
864
1242
  */
@@ -1229,6 +1607,248 @@ export function createExpressMiddleware(options = {}) {
1229
1607
  * @param {object} [options] - Options: companyName, department, format ('markdown'|'json'|'summary').
1230
1608
  * @returns {object|string}
1231
1609
  */
1610
+ export function formatCefEvent(eventData = {}) {
1611
+ const {
1612
+ vendor = 'PrivacyScrubber',
1613
+ product = 'ZTDS',
1614
+ version = '2.2.5',
1615
+ eventClassId = 'PII_REDACTION',
1616
+ eventName = 'Zero-Trust PII Sanitization Executed',
1617
+ riskLevel = 'LOW EXPOSURE',
1618
+ tokensMasked = 0,
1619
+ entities = {},
1620
+ frameworks = ['ZTDS Standard'],
1621
+ sessionHash = '',
1622
+ source = 'local_ram',
1623
+ action = 'redact_and_mask',
1624
+ organization = '',
1625
+ department = ''
1626
+ } = eventData;
1627
+
1628
+ let severity = 1;
1629
+ if (riskLevel.includes('CRITICAL')) severity = 9;
1630
+ else if (riskLevel.includes('MODERATE')) severity = 6;
1631
+ else if (riskLevel.includes('LOW')) severity = 3;
1632
+ else severity = 0;
1633
+
1634
+ const entitiesStr = Object.entries(entities).map(([k, v]) => `${k}:${v}`).join(',') || 'none';
1635
+ const frameworksStr = Array.isArray(frameworks) ? frameworks.join(',') : String(frameworks);
1636
+ const safeMsg = `Sanitized ${tokensMasked} entity tokens in volatile RAM. 0 bytes network egress.`;
1637
+
1638
+ const extension = [
1639
+ `src=${source}`,
1640
+ `act=${action}`,
1641
+ `cs1Label=RiskLevel cs1=${riskLevel.replace(/\s+/g, '_')}`,
1642
+ `cn1Label=TokensMasked cn1=${tokensMasked}`,
1643
+ `cs2Label=Frameworks cs2=${frameworksStr.replace(/\s+/g, '_')}`,
1644
+ `cs3Label=Entities cs3=${entitiesStr}`,
1645
+ sessionHash ? `cs4Label=SessionHash cs4=${sessionHash.substring(0, 16)}` : '',
1646
+ organization ? `suser=${organization.replace(/\s+/g, '_')}` : '',
1647
+ department ? `cs5Label=Department cs5=${department.replace(/\s+/g, '_')}` : '',
1648
+ `msg=${safeMsg}`
1649
+ ].filter(Boolean).join(' ');
1650
+
1651
+ return `CEF:0|${vendor}|${product}|${version}|${eventClassId}|${eventName}|${severity}|${extension}`;
1652
+ }
1653
+
1654
+ export function formatSyslogEvent(eventData = {}) {
1655
+ const {
1656
+ timestamp = new Date().toISOString(),
1657
+ hostname = (typeof os !== 'undefined' && typeof os.hostname === 'function' ? os.hostname() : 'localhost'),
1658
+ pid = (typeof process !== 'undefined' && process.pid ? process.pid : 1),
1659
+ riskLevel = 'LOW EXPOSURE',
1660
+ tokensMasked = 0,
1661
+ frameworks = ['ZTDS Standard'],
1662
+ sessionHash = '',
1663
+ organization = ''
1664
+ } = eventData;
1665
+
1666
+ let pri = 134; // local0.info
1667
+ if (riskLevel.includes('CRITICAL')) pri = 132; // local0.warning
1668
+ else if (riskLevel.includes('MODERATE')) pri = 133; // local0.notice
1669
+
1670
+ const frameworksStr = Array.isArray(frameworks) ? frameworks.join(';') : String(frameworks);
1671
+ const hashPrefix = sessionHash.substring(0, 16) || 'none';
1672
+ const orgStr = organization ? ` org="${organization.replace(/"/g, '')}"` : '';
1673
+
1674
+ const sd = `[ztds@49152 risk="${riskLevel}" tokens="${tokensMasked}" frameworks="${frameworksStr}" session="${hashPrefix}"${orgStr}]`;
1675
+ const msg = `Zero-Trust Data Sanitization executed in RAM. ${tokensMasked} token(s) masked. 0 bytes network egress.`;
1676
+
1677
+ return `<${pri}>1 ${timestamp} ${hostname} privacyscrubber ${pid} ZTDS - ${sd} ${msg}`;
1678
+ }
1679
+
1680
+ export function formatJsonlEvent(eventData = {}) {
1681
+ const {
1682
+ timestamp = new Date().toISOString(),
1683
+ version = '2.2.5',
1684
+ protocol = 'ZTDS',
1685
+ event = 'sanitization',
1686
+ riskLevel = 'LOW EXPOSURE',
1687
+ tokensMasked = 0,
1688
+ entities = {},
1689
+ frameworks = ['ZTDS Standard'],
1690
+ sessionHash = '',
1691
+ organization = '',
1692
+ department = '',
1693
+ executionMs = 0,
1694
+ metadata = {}
1695
+ } = eventData;
1696
+
1697
+ const record = {
1698
+ timestamp,
1699
+ version,
1700
+ protocol,
1701
+ event,
1702
+ risk_level: riskLevel,
1703
+ tokens_masked: tokensMasked,
1704
+ entities,
1705
+ frameworks,
1706
+ session_hash: sessionHash,
1707
+ zero_egress: true,
1708
+ verification_mode: "100% Offline (In-Memory RAM)",
1709
+ ...(organization && { organization }),
1710
+ ...(department && { department }),
1711
+ ...(executionMs && { execution_ms: executionMs }),
1712
+ ...(Object.keys(metadata).length > 0 && { metadata })
1713
+ };
1714
+
1715
+ return JSON.stringify(record);
1716
+ }
1717
+
1718
+ export function createAuditLogger(options = {}) {
1719
+ const {
1720
+ format = 'jsonl',
1721
+ destination = null,
1722
+ organization = 'PrivacyScrubber Enterprise Client',
1723
+ department = 'SecOps'
1724
+ } = options;
1725
+
1726
+ let fileHandle = null;
1727
+ let streamHandle = null;
1728
+ const buffer = [];
1729
+
1730
+ if (typeof destination === 'string' && destination.trim()) {
1731
+ try {
1732
+ const dir = path.dirname(destination);
1733
+ if (!fs.existsSync(dir)) {
1734
+ fs.mkdirSync(dir, { recursive: true });
1735
+ }
1736
+ fileHandle = destination;
1737
+ } catch {}
1738
+ } else if (destination && typeof destination.write === 'function') {
1739
+ streamHandle = destination;
1740
+ }
1741
+
1742
+ function formatEvent(eventData) {
1743
+ const enriched = {
1744
+ organization,
1745
+ department,
1746
+ timestamp: new Date().toISOString(),
1747
+ ...eventData
1748
+ };
1749
+
1750
+ if (format === 'cef') {
1751
+ return formatCefEvent(enriched);
1752
+ }
1753
+ if (format === 'syslog') {
1754
+ return formatSyslogEvent(enriched);
1755
+ }
1756
+ if (format === 'json') {
1757
+ return JSON.stringify(enriched, null, 2);
1758
+ }
1759
+ return formatJsonlEvent(enriched);
1760
+ }
1761
+
1762
+ function writeEntry(line) {
1763
+ buffer.push(line);
1764
+ if (fileHandle) {
1765
+ try {
1766
+ fs.appendFileSync(fileHandle, line + '\n', 'utf8');
1767
+ } catch (e) {
1768
+ console.error('[PrivacyScrubber AuditLogger] File write error:', e);
1769
+ }
1770
+ } else if (streamHandle) {
1771
+ try {
1772
+ streamHandle.write(line + '\n');
1773
+ } catch (e) {
1774
+ console.error('[PrivacyScrubber AuditLogger] Stream write error:', e);
1775
+ }
1776
+ }
1777
+ }
1778
+
1779
+ function logSanitization(result, context = {}) {
1780
+ if (!result) return '';
1781
+ let tokenMap = {};
1782
+ let telemetry = null;
1783
+ let count = 0;
1784
+ let execMs = 0;
1785
+
1786
+ if (result.tokenMap && typeof result.tokenMap === 'object') {
1787
+ tokenMap = result.tokenMap;
1788
+ telemetry = result.telemetry;
1789
+ count = result.count !== undefined ? result.count : Object.keys(tokenMap).length;
1790
+ execMs = result.executionMs || 0;
1791
+ } else if (typeof result === 'object') {
1792
+ tokenMap = result;
1793
+ count = result.count !== undefined ? result.count : (result.tokensMasked !== undefined ? result.tokensMasked : Object.keys(tokenMap).length);
1794
+ }
1795
+
1796
+ if (!telemetry) {
1797
+ telemetry = buildAuditTelemetry(tokenMap);
1798
+ }
1799
+
1800
+ let sessionHash = '';
1801
+ try {
1802
+ const cryptoMod = require('crypto');
1803
+ sessionHash = cryptoMod.createHash('sha256').update(JSON.stringify(tokenMap) + Date.now()).digest('hex');
1804
+ } catch {
1805
+ sessionHash = 'ztds_' + Math.random().toString(16).substring(2, 10);
1806
+ }
1807
+
1808
+ const eventData = {
1809
+ event: 'pii_sanitization',
1810
+ riskLevel: telemetry.riskLevel || 'CLEAN (ZERO PII)',
1811
+ tokensMasked: count,
1812
+ entities: telemetry.entities || {},
1813
+ frameworks: telemetry.frameworksList || ['ZTDS Standard'],
1814
+ sessionHash,
1815
+ executionMs: execMs,
1816
+ metadata: context.metadata || {}
1817
+ };
1818
+
1819
+ const formatted = formatEvent(eventData);
1820
+ writeEntry(formatted);
1821
+ return formatted;
1822
+ }
1823
+
1824
+ function logEvent(eventData) {
1825
+ const formatted = formatEvent(eventData);
1826
+ writeEntry(formatted);
1827
+ return formatted;
1828
+ }
1829
+
1830
+ function getEntries() {
1831
+ return [...buffer];
1832
+ }
1833
+
1834
+ function clear() {
1835
+ buffer.length = 0;
1836
+ }
1837
+
1838
+ return {
1839
+ formatEvent,
1840
+ logSanitization,
1841
+ logEvent,
1842
+ log: logEvent,
1843
+ getEntries,
1844
+ getBuffer: getEntries,
1845
+ clear,
1846
+ clearBuffer: clear,
1847
+ format,
1848
+ destination
1849
+ };
1850
+ }
1851
+
1232
1852
  export function generateAuditReport(sessionMap = {}, options = {}) {
1233
1853
  const { companyName = 'PrivacyScrubber Client', department = 'SecOps / Compliance', format = 'markdown' } = options;
1234
1854
  const telemetry = buildAuditTelemetry(sessionMap);
@@ -1244,6 +1864,45 @@ export function generateAuditReport(sessionMap = {}, options = {}) {
1244
1864
  const timestamp = new Date().toISOString();
1245
1865
  const entitySummary = Object.entries(telemetry.entities).map(([t, count]) => `[${t}]: ${count}`).join(', ') || 'None (Clean)';
1246
1866
 
1867
+ if (format === 'cef') {
1868
+ return formatCefEvent({
1869
+ organization: companyName,
1870
+ department: department,
1871
+ timestamp,
1872
+ sessionHash,
1873
+ riskLevel: telemetry.riskLevel,
1874
+ tokensMasked: telemetry.totalCount,
1875
+ entities: telemetry.entities,
1876
+ frameworks: telemetry.frameworksList
1877
+ });
1878
+ }
1879
+
1880
+ if (format === 'syslog') {
1881
+ return formatSyslogEvent({
1882
+ organization: companyName,
1883
+ department: department,
1884
+ timestamp,
1885
+ sessionHash,
1886
+ riskLevel: telemetry.riskLevel,
1887
+ tokensMasked: telemetry.totalCount,
1888
+ entities: telemetry.entities,
1889
+ frameworks: telemetry.frameworksList
1890
+ });
1891
+ }
1892
+
1893
+ if (format === 'jsonl') {
1894
+ return formatJsonlEvent({
1895
+ organization: companyName,
1896
+ department: department,
1897
+ timestamp,
1898
+ sessionHash,
1899
+ riskLevel: telemetry.riskLevel,
1900
+ tokensMasked: telemetry.totalCount,
1901
+ entities: telemetry.entities,
1902
+ frameworks: telemetry.frameworksList
1903
+ });
1904
+ }
1905
+
1247
1906
  if (format === 'json') {
1248
1907
  return {
1249
1908
  protocol: "Zero-Trust Data Sanitization (ZTDS)",
@@ -1676,11 +2335,19 @@ export default {
1676
2335
  buildAuditTelemetry,
1677
2336
  formatAuditReceipt,
1678
2337
  generateAuditReport,
2338
+ formatCefEvent,
2339
+ formatSyslogEvent,
2340
+ formatJsonlEvent,
2341
+ createAuditLogger,
1679
2342
  generateProcurementMemo,
1680
2343
  PrivacyScrubberEngine,
1681
2344
  PrivacyScrubberLicenseError,
1682
2345
  wrapOpenAI,
1683
2346
  createLangChainTransform,
2347
+ createDocumentTransformer,
2348
+ createLangChainDocumentTransformer,
2349
+ createLlamaIndexTransform,
2350
+ createVectorIngestionGuard,
1684
2351
  StreamingDetokenizer,
1685
2352
  createStreamTokenRestorer,
1686
2353
  createNodeStreamTokenRestorer,