@privacyscrubber/sdk 2.4.5 → 2.4.7
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 +50 -1
- package/browser.js +431 -26
- package/cli.js +119 -11
- package/examples/python-bridge/README.md +3 -3
- package/examples/python-bridge/fastapi_example.py +1 -1
- package/examples/python-bridge/langchain_example.py +1 -1
- package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +2 -2
- package/examples/python-bridge/privacyscrubber/cli.py +1 -1
- package/examples/python-bridge/pyproject.toml +1 -1
- package/examples/python-bridge/setup.py +1 -1
- package/fastmcp.cjs +56 -0
- package/fastmcp.d.ts +5 -0
- package/index.cjs +1186 -88
- package/index.d.ts +156 -2
- package/index.js +1186 -88
- package/package.json +13 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/sarif.cjs +14 -2
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +123 -34
- package/vercel-ai.d.ts +7 -0
- package/benchmark.js +0 -355
- package/examples/python-bridge/bridge_daemon.cjs +0 -260
- package/examples/python-bridge/langchain_adapter.py +0 -20
- package/examples/python-bridge/privacyscrubber.py +0 -36
- package/examples/python-bridge/test_python_bridge.py +0 -320
package/index.cjs
CHANGED
|
@@ -32,6 +32,16 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
|
|
|
32
32
|
scrubberPkg.init();
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const SDK_VERSION = (() => {
|
|
36
|
+
try {
|
|
37
|
+
const pkgJson = require('./package.json');
|
|
38
|
+
if (pkgJson && typeof pkgJson.version === 'string') {
|
|
39
|
+
return pkgJson.version;
|
|
40
|
+
}
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
return '2.4.7';
|
|
43
|
+
})();
|
|
44
|
+
|
|
35
45
|
/**
|
|
36
46
|
* Built-in patterns for DevOps & Cloud Secrets detection.
|
|
37
47
|
*/
|
|
@@ -78,7 +88,10 @@ class PrivacyScrubberLicenseError extends Error {
|
|
|
78
88
|
this.currentLength = currentLength;
|
|
79
89
|
this.limit = limit;
|
|
80
90
|
this.profile = profile;
|
|
81
|
-
|
|
91
|
+
const safeProf = encodeURIComponent(String(profile || 'General').toLowerCase());
|
|
92
|
+
this.upgradeUrl = `https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
|
|
93
|
+
this.trialUrl = `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
|
|
94
|
+
this.trialCommand = 'npx @privacyscrubber/sdk trial';
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
|
|
@@ -185,11 +198,27 @@ function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
185
198
|
trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
|
|
186
199
|
};
|
|
187
200
|
|
|
201
|
+
const agenticProtocol = {
|
|
202
|
+
protocolsSupported: ['MCP-1.0 (stdio/SSE)', 'A2A-1.0 (Agent2Agent)', 'ZTDS-RFC-v1.0'],
|
|
203
|
+
agentCardUrl: 'https://privacyscrubber.com/.well-known/agent-card.json',
|
|
204
|
+
ztdsStandardCardUrl: 'https://ztds.ai/.well-known/agent-card.json',
|
|
205
|
+
regulatorySafeHarbor: [
|
|
206
|
+
'GDPR-Art-4(5)-Pseudonymization',
|
|
207
|
+
'GDPR-Art-25-Privacy-by-Design',
|
|
208
|
+
'GDPR-Art-32-TOMs',
|
|
209
|
+
'EU-AI-Act-Art-50',
|
|
210
|
+
'EU-AI-Act-Art-17-EN-18286-QMS',
|
|
211
|
+
'CNIL-ICO-Agentic-Memory-Compartmentalization',
|
|
212
|
+
'IL-PPL-Amendment-13-Sec-17'
|
|
213
|
+
],
|
|
214
|
+
zeroCleartextEgress: true
|
|
215
|
+
};
|
|
216
|
+
|
|
188
217
|
const statutoryCitations = [
|
|
189
218
|
{
|
|
190
|
-
statute: 'EU AI Act (Art. 50)',
|
|
191
|
-
scope: 'AI Transparency &
|
|
192
|
-
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
|
|
219
|
+
statute: 'EU AI Act (Art. 50 & Art. 17 / EN 18286:2026 QMS)',
|
|
220
|
+
scope: 'AI Transparency, Data Minimization & Harmonized QMS Governance',
|
|
221
|
+
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models and supports Article 17 Quality Management System (EN 18286:2026) data governance controls. Informational mapping, not a compliance determination.'
|
|
193
222
|
},
|
|
194
223
|
{
|
|
195
224
|
statute: 'GDPR (Art. 4, 25, 28, 32)',
|
|
@@ -221,6 +250,7 @@ function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
221
250
|
frameworksList: Array.from(frameworksSet),
|
|
222
251
|
statutoryCitations,
|
|
223
252
|
ipHoldings,
|
|
253
|
+
agenticProtocol,
|
|
224
254
|
multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
|
|
225
255
|
};
|
|
226
256
|
}
|
|
@@ -529,7 +559,8 @@ function sanitize(text, options = {}) {
|
|
|
529
559
|
if (!isPro && text.length > limit) {
|
|
530
560
|
if (shouldThrow) {
|
|
531
561
|
const expiredPrefix = isExpiredKey ? `[PILOT/TRIAL EXPIRED: ${licenseCheck.error}] ` : '';
|
|
532
|
-
const
|
|
562
|
+
const safeProf = encodeURIComponent(String(profile || 'General').toLowerCase());
|
|
563
|
+
const errorMsg = `[PrivacyScrubber] ${expiredPrefix}Free Tier character limit exceeded (${limit.toLocaleString()} chars for profile '${profile}', received ${text.length.toLocaleString()} chars). Upgrade to Developer SDK ($299/mo) for unlimited throughput: https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&src=${isExpiredKey ? 'pilot_expired' : 'dev_limit'} (Start 7-day B2B trial: 'npx @privacyscrubber/sdk trial' or 'npx @privacyscrubber/sdk memo' for CISO memo)`;
|
|
533
564
|
throw new PrivacyScrubberLicenseError(errorMsg, {
|
|
534
565
|
tier: 'FREE',
|
|
535
566
|
currentLength: text.length,
|
|
@@ -610,12 +641,17 @@ function sanitize(text, options = {}) {
|
|
|
610
641
|
tier: tier,
|
|
611
642
|
limit: limit,
|
|
612
643
|
wasTruncated: wasTruncated,
|
|
644
|
+
truncated: wasTruncated,
|
|
613
645
|
...(isSeatTier && {
|
|
614
|
-
upgradeUrl:
|
|
646
|
+
upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
|
|
647
|
+
trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=seat_tier_mismatch`,
|
|
648
|
+
trialCommand: 'npx @privacyscrubber/sdk trial',
|
|
615
649
|
recommendation: `Your ${rawTier} license is valid for interactive browser seats only. Upgrade to Developer SDK ($299/mo) for unlimited backend server throughput: https://privacyscrubber.com/pricing?tier=sdk`
|
|
616
650
|
}),
|
|
617
651
|
...(!isPro && !isSeatTier && {
|
|
618
|
-
upgradeUrl:
|
|
652
|
+
upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
|
|
653
|
+
trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General').toLowerCase())}&src=dev_upsell`,
|
|
654
|
+
trialCommand: 'npx @privacyscrubber/sdk trial',
|
|
619
655
|
recommendation: 'Upgrade to Developer SDK ($299/mo) for unlimited backend text processing, custom regex rules, and specialized compliance profiles.'
|
|
620
656
|
})
|
|
621
657
|
},
|
|
@@ -722,6 +758,21 @@ function sanitizeObject(obj, options = {}) {
|
|
|
722
758
|
|
|
723
759
|
const mergedSessionMap = { ...(options.existingSessionMap || {}) };
|
|
724
760
|
let totalCount = 0;
|
|
761
|
+
const seen = new WeakMap();
|
|
762
|
+
|
|
763
|
+
function isNonPlainObject(val) {
|
|
764
|
+
return (
|
|
765
|
+
val instanceof Date ||
|
|
766
|
+
val instanceof RegExp ||
|
|
767
|
+
val instanceof Error ||
|
|
768
|
+
val instanceof Promise ||
|
|
769
|
+
val instanceof WeakMap ||
|
|
770
|
+
val instanceof WeakSet ||
|
|
771
|
+
(typeof URL !== 'undefined' && val instanceof URL) ||
|
|
772
|
+
ArrayBuffer.isView(val) ||
|
|
773
|
+
(typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
|
|
774
|
+
);
|
|
775
|
+
}
|
|
725
776
|
|
|
726
777
|
function traverseAndSanitize(val) {
|
|
727
778
|
if (val === null || val === undefined) return val;
|
|
@@ -736,20 +787,42 @@ function sanitizeObject(obj, options = {}) {
|
|
|
736
787
|
totalCount += res.count;
|
|
737
788
|
return res.scrubbedText;
|
|
738
789
|
}
|
|
790
|
+
if (typeof val !== 'object') return val;
|
|
791
|
+
if (isNonPlainObject(val)) return val;
|
|
792
|
+
if (seen.has(val)) return seen.get(val);
|
|
793
|
+
|
|
739
794
|
if (Array.isArray(val)) {
|
|
740
|
-
|
|
795
|
+
const outArr = [];
|
|
796
|
+
seen.set(val, outArr);
|
|
797
|
+
for (let i = 0; i < val.length; i++) {
|
|
798
|
+
outArr.push(traverseAndSanitize(val[i]));
|
|
799
|
+
}
|
|
800
|
+
return outArr;
|
|
741
801
|
}
|
|
742
|
-
if (val instanceof
|
|
743
|
-
|
|
802
|
+
if (val instanceof Map) {
|
|
803
|
+
const outMap = new Map();
|
|
804
|
+
seen.set(val, outMap);
|
|
805
|
+
for (const [k, v] of val.entries()) {
|
|
806
|
+
outMap.set(k, traverseAndSanitize(v));
|
|
807
|
+
}
|
|
808
|
+
return outMap;
|
|
744
809
|
}
|
|
745
|
-
if (
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
810
|
+
if (val instanceof Set) {
|
|
811
|
+
const outSet = new Set();
|
|
812
|
+
seen.set(val, outSet);
|
|
813
|
+
for (const item of val.values()) {
|
|
814
|
+
outSet.add(traverseAndSanitize(item));
|
|
749
815
|
}
|
|
750
|
-
return
|
|
816
|
+
return outSet;
|
|
751
817
|
}
|
|
752
|
-
|
|
818
|
+
|
|
819
|
+
const out = {};
|
|
820
|
+
seen.set(val, out);
|
|
821
|
+
for (const [k, v] of Object.entries(val)) {
|
|
822
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
823
|
+
out[k] = traverseAndSanitize(v);
|
|
824
|
+
}
|
|
825
|
+
return out;
|
|
753
826
|
}
|
|
754
827
|
|
|
755
828
|
const sanitizedResult = traverseAndSanitize(obj);
|
|
@@ -779,23 +852,62 @@ function sanitizeObject(obj, options = {}) {
|
|
|
779
852
|
* @param {Record<string, string>} tokenMap - Token map returned from sanitize() or sanitizeObject().
|
|
780
853
|
* @returns {any} - Restored object with original values.
|
|
781
854
|
*/
|
|
782
|
-
function restoreObject(obj, tokenMap = {}) {
|
|
855
|
+
function restoreObject(obj, tokenMap = {}, _seen = null) {
|
|
783
856
|
if (obj === null || obj === undefined) return obj;
|
|
784
857
|
if (typeof obj === 'string') {
|
|
785
858
|
const res = restore(obj, tokenMap);
|
|
786
859
|
return res.restoredText;
|
|
787
860
|
}
|
|
861
|
+
if (typeof obj !== 'object') return obj;
|
|
862
|
+
if (
|
|
863
|
+
obj instanceof Date ||
|
|
864
|
+
obj instanceof RegExp ||
|
|
865
|
+
obj instanceof Error ||
|
|
866
|
+
obj instanceof Promise ||
|
|
867
|
+
obj instanceof WeakMap ||
|
|
868
|
+
obj instanceof WeakSet ||
|
|
869
|
+
(typeof URL !== 'undefined' && obj instanceof URL) ||
|
|
870
|
+
ArrayBuffer.isView(obj) ||
|
|
871
|
+
(typeof ArrayBuffer !== 'undefined' && obj instanceof ArrayBuffer)
|
|
872
|
+
) {
|
|
873
|
+
return obj;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const seen = _seen || new WeakMap();
|
|
877
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
878
|
+
|
|
788
879
|
if (Array.isArray(obj)) {
|
|
789
|
-
|
|
880
|
+
const outArr = [];
|
|
881
|
+
seen.set(obj, outArr);
|
|
882
|
+
for (let i = 0; i < obj.length; i++) {
|
|
883
|
+
outArr.push(restoreObject(obj[i], tokenMap, seen));
|
|
884
|
+
}
|
|
885
|
+
return outArr;
|
|
790
886
|
}
|
|
791
|
-
if (
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
887
|
+
if (obj instanceof Map) {
|
|
888
|
+
const outMap = new Map();
|
|
889
|
+
seen.set(obj, outMap);
|
|
890
|
+
for (const [k, v] of obj.entries()) {
|
|
891
|
+
outMap.set(k, restoreObject(v, tokenMap, seen));
|
|
795
892
|
}
|
|
796
|
-
return
|
|
893
|
+
return outMap;
|
|
894
|
+
}
|
|
895
|
+
if (obj instanceof Set) {
|
|
896
|
+
const outSet = new Set();
|
|
897
|
+
seen.set(obj, outSet);
|
|
898
|
+
for (const item of obj.values()) {
|
|
899
|
+
outSet.add(restoreObject(item, tokenMap, seen));
|
|
900
|
+
}
|
|
901
|
+
return outSet;
|
|
797
902
|
}
|
|
798
|
-
|
|
903
|
+
|
|
904
|
+
const out = {};
|
|
905
|
+
seen.set(obj, out);
|
|
906
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
907
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
908
|
+
out[k] = restoreObject(v, tokenMap, seen);
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
799
911
|
}
|
|
800
912
|
|
|
801
913
|
/**
|
|
@@ -838,12 +950,16 @@ class PrivacyScrubberEngine {
|
|
|
838
950
|
.forEach(v => mergedIgnoreList.add(v));
|
|
839
951
|
}
|
|
840
952
|
|
|
953
|
+
const effectiveExistingMap = options.persistSession !== false
|
|
954
|
+
? { ...this.sessionMap, ...(options.existingSessionMap || {}) }
|
|
955
|
+
: (options.existingSessionMap || {});
|
|
956
|
+
|
|
841
957
|
const mergedOptions = {
|
|
842
958
|
profile: options.profile || this.defaultProfile,
|
|
843
959
|
licenseKey: options.licenseKey || this.licenseKey,
|
|
844
960
|
customRules: [...this.customRules, ...(options.customRules || [])],
|
|
845
961
|
tokenLabelMap: { ...this.tokenLabelMap, ...(options.tokenLabelMap || {}) },
|
|
846
|
-
existingSessionMap:
|
|
962
|
+
existingSessionMap: effectiveExistingMap,
|
|
847
963
|
generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
|
|
848
964
|
throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
|
|
849
965
|
detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
|
|
@@ -943,68 +1059,229 @@ class PrivacyScrubberEngine {
|
|
|
943
1059
|
* @returns {object} Wrapped OpenAI client.
|
|
944
1060
|
*/
|
|
945
1061
|
function wrapOpenAI(openaiClient, options = {}) {
|
|
946
|
-
|
|
1062
|
+
const hasChatCompletions = !!(openaiClient && openaiClient.chat && openaiClient.chat.completions && typeof openaiClient.chat.completions.create === 'function');
|
|
1063
|
+
const hasResponses = !!(openaiClient && openaiClient.responses && typeof openaiClient.responses.create === 'function');
|
|
1064
|
+
if (!hasChatCompletions && !hasResponses) {
|
|
947
1065
|
throw new Error('[PrivacyScrubber] Invalid OpenAI client instance passed to wrapOpenAI.');
|
|
948
1066
|
}
|
|
949
1067
|
|
|
950
1068
|
const engine = new PrivacyScrubberEngine(options);
|
|
951
|
-
const originalCreate = openaiClient.chat.completions.create.bind(openaiClient.chat.completions);
|
|
952
1069
|
|
|
953
|
-
|
|
954
|
-
if (
|
|
955
|
-
|
|
1070
|
+
function sanitizeMessageContent(content, sessionTokenMap) {
|
|
1071
|
+
if (typeof content === 'string') {
|
|
1072
|
+
const res = engine.sanitize(content, { existingSessionMap: sessionTokenMap });
|
|
1073
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1074
|
+
return res.scrubbedText;
|
|
956
1075
|
}
|
|
1076
|
+
if (Array.isArray(content)) {
|
|
1077
|
+
return content.map(chunk => {
|
|
1078
|
+
if (chunk && (chunk.type === 'text' || chunk.type === 'input_text') && typeof chunk.text === 'string') {
|
|
1079
|
+
const res = engine.sanitize(chunk.text, { existingSessionMap: sessionTokenMap });
|
|
1080
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1081
|
+
return { ...chunk, text: res.scrubbedText };
|
|
1082
|
+
}
|
|
1083
|
+
return chunk;
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
return content;
|
|
1087
|
+
}
|
|
957
1088
|
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
1089
|
+
if (hasChatCompletions) {
|
|
1090
|
+
const originalCreate = openaiClient.chat.completions.create.bind(openaiClient.chat.completions);
|
|
1091
|
+
|
|
1092
|
+
openaiClient.chat.completions.create = async function(params, requestOptions) {
|
|
1093
|
+
if (!params || !Array.isArray(params.messages)) {
|
|
1094
|
+
return originalCreate(params, requestOptions);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const sessionTokenMap = options.persistSession === false ? {} : { ...engine.sessionMap };
|
|
1098
|
+
const sanitizedMessages = params.messages.map(msg => ({
|
|
1099
|
+
...msg,
|
|
1100
|
+
content: sanitizeMessageContent(msg.content, sessionTokenMap)
|
|
1101
|
+
}));
|
|
1102
|
+
|
|
1103
|
+
const modifiedParams = { ...params, messages: sanitizedMessages };
|
|
1104
|
+
const response = await originalCreate(modifiedParams, requestOptions);
|
|
1105
|
+
|
|
1106
|
+
// Streaming support: transparently de-tokenize async iterable chunks
|
|
1107
|
+
if (params.stream) {
|
|
1108
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1109
|
+
return detok.wrapStream(response);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (response && Array.isArray(response.choices)) {
|
|
1113
|
+
response.choices.forEach(choice => {
|
|
1114
|
+
if (choice.message) {
|
|
1115
|
+
if (typeof choice.message.content === 'string') {
|
|
1116
|
+
choice.message.content = engine.restore(choice.message.content, sessionTokenMap).restoredText;
|
|
1117
|
+
} else if (Array.isArray(choice.message.content)) {
|
|
1118
|
+
choice.message.content = choice.message.content.map(chunk => {
|
|
1119
|
+
if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
|
|
1120
|
+
return { ...chunk, text: engine.restore(chunk.text, sessionTokenMap).restoredText };
|
|
1121
|
+
}
|
|
1122
|
+
return chunk;
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return response;
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (hasResponses) {
|
|
1134
|
+
const originalResponsesCreate = openaiClient.responses.create.bind(openaiClient.responses);
|
|
1135
|
+
|
|
1136
|
+
openaiClient.responses.create = async function(params, requestOptions) {
|
|
1137
|
+
if (!params || typeof params !== 'object') {
|
|
1138
|
+
return originalResponsesCreate(params, requestOptions);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const sessionTokenMap = options.persistSession === false ? {} : { ...engine.sessionMap };
|
|
1142
|
+
const modifiedParams = { ...params };
|
|
1143
|
+
|
|
1144
|
+
if (typeof modifiedParams.instructions === 'string') {
|
|
1145
|
+
const res = engine.sanitize(modifiedParams.instructions, { existingSessionMap: sessionTokenMap });
|
|
962
1146
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
963
|
-
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1147
|
+
modifiedParams.instructions = res.scrubbedText;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (typeof modifiedParams.input === 'string') {
|
|
1151
|
+
const res = engine.sanitize(modifiedParams.input, { existingSessionMap: sessionTokenMap });
|
|
1152
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1153
|
+
modifiedParams.input = res.scrubbedText;
|
|
1154
|
+
} else if (Array.isArray(modifiedParams.input)) {
|
|
1155
|
+
modifiedParams.input = modifiedParams.input.map(item => {
|
|
1156
|
+
if (typeof item === 'string') {
|
|
1157
|
+
const res = engine.sanitize(item, { existingSessionMap: sessionTokenMap });
|
|
968
1158
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
969
|
-
return
|
|
1159
|
+
return res.scrubbedText;
|
|
1160
|
+
}
|
|
1161
|
+
if (item && typeof item === 'object') {
|
|
1162
|
+
return {
|
|
1163
|
+
...item,
|
|
1164
|
+
...(item.content !== undefined && { content: sanitizeMessageContent(item.content, sessionTokenMap) }),
|
|
1165
|
+
...(typeof item.text === 'string' && {
|
|
1166
|
+
text: (() => {
|
|
1167
|
+
const res = engine.sanitize(item.text, { existingSessionMap: sessionTokenMap });
|
|
1168
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1169
|
+
return res.scrubbedText;
|
|
1170
|
+
})()
|
|
1171
|
+
})
|
|
1172
|
+
};
|
|
970
1173
|
}
|
|
971
|
-
return
|
|
1174
|
+
return item;
|
|
972
1175
|
});
|
|
973
|
-
return { ...msg, content: sanitizedChunks };
|
|
974
1176
|
}
|
|
975
|
-
return msg;
|
|
976
|
-
});
|
|
977
1177
|
|
|
978
|
-
|
|
1178
|
+
const response = await originalResponsesCreate(modifiedParams, requestOptions);
|
|
1179
|
+
|
|
1180
|
+
if (params.stream) {
|
|
1181
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1182
|
+
return detok.wrapStream(response);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (response && typeof response === 'object') {
|
|
1186
|
+
if (typeof response.output_text === 'string') {
|
|
1187
|
+
try {
|
|
1188
|
+
response.output_text = engine.restore(response.output_text, sessionTokenMap).restoredText;
|
|
1189
|
+
} catch (_) {}
|
|
1190
|
+
}
|
|
1191
|
+
if (Array.isArray(response.output)) {
|
|
1192
|
+
response.output.forEach(outItem => {
|
|
1193
|
+
if (outItem && Array.isArray(outItem.content)) {
|
|
1194
|
+
outItem.content.forEach(block => {
|
|
1195
|
+
if (block && typeof block.text === 'string') {
|
|
1196
|
+
block.text = engine.restore(block.text, sessionTokenMap).restoredText;
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return response;
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return openaiClient;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Transparent Zero-Trust wrapper for Anthropic SDK (@anthropic-ai/sdk).
|
|
1213
|
+
* Automatically scrubs system prompts and user/assistant messages in local RAM
|
|
1214
|
+
* before sending to Anthropic, and restores masked tokens in both synchronous
|
|
1215
|
+
* and streaming responses.
|
|
1216
|
+
*
|
|
1217
|
+
* @param {object} anthropicClient - Instance of Anthropic SDK (e.g. new Anthropic()).
|
|
1218
|
+
* @param {object} [options={}] - PrivacyScrubber configuration.
|
|
1219
|
+
* @returns {object} Wrapped Anthropic client.
|
|
1220
|
+
*/
|
|
1221
|
+
function wrapAnthropic(anthropicClient, options = {}) {
|
|
1222
|
+
if (!anthropicClient || !anthropicClient.messages || typeof anthropicClient.messages.create !== 'function') {
|
|
1223
|
+
throw new Error('[PrivacyScrubber] Invalid Anthropic client instance passed to wrapAnthropic.');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const engine = new PrivacyScrubberEngine(options);
|
|
1227
|
+
const originalCreate = anthropicClient.messages.create.bind(anthropicClient.messages);
|
|
1228
|
+
|
|
1229
|
+
function sanitizeAnthropicContent(content, sessionTokenMap) {
|
|
1230
|
+
if (typeof content === 'string') {
|
|
1231
|
+
const res = engine.sanitize(content, { existingSessionMap: sessionTokenMap });
|
|
1232
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1233
|
+
return res.scrubbedText;
|
|
1234
|
+
}
|
|
1235
|
+
if (Array.isArray(content)) {
|
|
1236
|
+
return content.map(block => {
|
|
1237
|
+
if (block && block.type === 'text' && typeof block.text === 'string') {
|
|
1238
|
+
const res = engine.sanitize(block.text, { existingSessionMap: sessionTokenMap });
|
|
1239
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1240
|
+
return { ...block, text: res.scrubbedText };
|
|
1241
|
+
}
|
|
1242
|
+
return block;
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
return content;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
anthropicClient.messages.create = async function(params, requestOptions) {
|
|
1249
|
+
if (!params || !Array.isArray(params.messages)) {
|
|
1250
|
+
return originalCreate(params, requestOptions);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const sessionTokenMap = {};
|
|
1254
|
+
const modifiedParams = { ...params };
|
|
1255
|
+
|
|
1256
|
+
if (modifiedParams.system !== undefined) {
|
|
1257
|
+
modifiedParams.system = sanitizeAnthropicContent(modifiedParams.system, sessionTokenMap);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
modifiedParams.messages = params.messages.map(msg => ({
|
|
1261
|
+
...msg,
|
|
1262
|
+
content: sanitizeAnthropicContent(msg.content, sessionTokenMap)
|
|
1263
|
+
}));
|
|
1264
|
+
|
|
979
1265
|
const response = await originalCreate(modifiedParams, requestOptions);
|
|
980
1266
|
|
|
981
|
-
// Streaming support: transparently de-tokenize async iterable chunks
|
|
982
1267
|
if (params.stream) {
|
|
983
1268
|
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
984
1269
|
return detok.wrapStream(response);
|
|
985
1270
|
}
|
|
986
1271
|
|
|
987
|
-
if (response && Array.isArray(response.
|
|
988
|
-
response.
|
|
989
|
-
if (
|
|
990
|
-
|
|
991
|
-
choice.message.content = engine.restore(choice.message.content, sessionTokenMap).restoredText;
|
|
992
|
-
} else if (Array.isArray(choice.message.content)) {
|
|
993
|
-
choice.message.content = choice.message.content.map(chunk => {
|
|
994
|
-
if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
|
|
995
|
-
return { ...chunk, text: engine.restore(chunk.text, sessionTokenMap).restoredText };
|
|
996
|
-
}
|
|
997
|
-
return chunk;
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1272
|
+
if (response && Array.isArray(response.content)) {
|
|
1273
|
+
response.content = response.content.map(block => {
|
|
1274
|
+
if (block && block.type === 'text' && typeof block.text === 'string') {
|
|
1275
|
+
return { ...block, text: engine.restore(block.text, sessionTokenMap).restoredText };
|
|
1000
1276
|
}
|
|
1277
|
+
return block;
|
|
1001
1278
|
});
|
|
1002
1279
|
}
|
|
1003
1280
|
|
|
1004
1281
|
return response;
|
|
1005
1282
|
};
|
|
1006
1283
|
|
|
1007
|
-
return
|
|
1284
|
+
return anthropicClient;
|
|
1008
1285
|
}
|
|
1009
1286
|
|
|
1010
1287
|
/**
|
|
@@ -1023,25 +1300,43 @@ function wrapAiModel(model, options = {}) {
|
|
|
1023
1300
|
throw new Error('[PrivacyScrubber] Invalid model instance passed to wrapAiModel.');
|
|
1024
1301
|
}
|
|
1025
1302
|
|
|
1026
|
-
const engine = new PrivacyScrubberEngine(options);
|
|
1303
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1304
|
+
const sharedSessionMap = {};
|
|
1027
1305
|
const wrapped = Object.create(model);
|
|
1028
1306
|
|
|
1029
1307
|
function sanitizePromptParts(prompt, sessionMap) {
|
|
1308
|
+
if (typeof prompt === 'string') {
|
|
1309
|
+
const res = engine.sanitize(prompt, { existingSessionMap: sessionMap, persistSession: false });
|
|
1310
|
+
Object.assign(sessionMap, res.tokenMap);
|
|
1311
|
+
return res.scrubbedText;
|
|
1312
|
+
}
|
|
1030
1313
|
if (!Array.isArray(prompt)) return prompt;
|
|
1031
1314
|
return prompt.map(msg => {
|
|
1032
1315
|
if (!msg) return msg;
|
|
1033
1316
|
if (typeof msg.content === 'string') {
|
|
1034
|
-
const res = engine.sanitize(msg.content, { existingSessionMap: sessionMap });
|
|
1317
|
+
const res = engine.sanitize(msg.content, { existingSessionMap: sessionMap, persistSession: false });
|
|
1035
1318
|
Object.assign(sessionMap, res.tokenMap);
|
|
1036
1319
|
return { ...msg, content: res.scrubbedText };
|
|
1037
1320
|
}
|
|
1038
1321
|
if (Array.isArray(msg.content)) {
|
|
1039
1322
|
const newContent = msg.content.map(part => {
|
|
1040
|
-
if (part && part.type === 'text' && typeof part.text === 'string') {
|
|
1041
|
-
const res = engine.sanitize(part.text, { existingSessionMap: sessionMap });
|
|
1323
|
+
if (part && (part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string') {
|
|
1324
|
+
const res = engine.sanitize(part.text, { existingSessionMap: sessionMap, persistSession: false });
|
|
1042
1325
|
Object.assign(sessionMap, res.tokenMap);
|
|
1043
1326
|
return { ...part, text: res.scrubbedText };
|
|
1044
1327
|
}
|
|
1328
|
+
if (part && part.type === 'tool-result') {
|
|
1329
|
+
if (typeof part.result === 'string') {
|
|
1330
|
+
const res = engine.sanitize(part.result, { existingSessionMap: sessionMap, persistSession: false });
|
|
1331
|
+
Object.assign(sessionMap, res.tokenMap);
|
|
1332
|
+
return { ...part, result: res.scrubbedText };
|
|
1333
|
+
}
|
|
1334
|
+
if (part.output && typeof part.output.value === 'string') {
|
|
1335
|
+
const res = engine.sanitize(part.output.value, { existingSessionMap: sessionMap, persistSession: false });
|
|
1336
|
+
Object.assign(sessionMap, res.tokenMap);
|
|
1337
|
+
return { ...part, output: { ...part.output, value: res.scrubbedText } };
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1045
1340
|
return part;
|
|
1046
1341
|
});
|
|
1047
1342
|
return { ...msg, content: newContent };
|
|
@@ -1050,16 +1345,41 @@ function wrapAiModel(model, options = {}) {
|
|
|
1050
1345
|
});
|
|
1051
1346
|
}
|
|
1052
1347
|
|
|
1348
|
+
function createScopedTokenMap(prompt) {
|
|
1349
|
+
const sessionTokenMap = options.persistSession ? { ...sharedSessionMap } : {};
|
|
1350
|
+
if (!options.persistSession && prompt && Object.keys(sharedSessionMap).length > 0) {
|
|
1351
|
+
const s = typeof prompt === 'string' ? prompt : JSON.stringify(prompt);
|
|
1352
|
+
for (const [tok, orig] of Object.entries(sharedSessionMap)) {
|
|
1353
|
+
if (s.includes(tok)) sessionTokenMap[tok] = orig;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return sessionTokenMap;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1053
1359
|
if (typeof model.doGenerate === 'function') {
|
|
1054
1360
|
wrapped.doGenerate = async function(params) {
|
|
1055
|
-
const sessionTokenMap =
|
|
1361
|
+
const sessionTokenMap = createScopedTokenMap(params?.prompt);
|
|
1056
1362
|
const sanitizedParams = {
|
|
1057
1363
|
...params,
|
|
1058
1364
|
prompt: sanitizePromptParts(params.prompt, sessionTokenMap)
|
|
1059
1365
|
};
|
|
1366
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1060
1367
|
const result = await model.doGenerate(sanitizedParams);
|
|
1061
|
-
if (result
|
|
1062
|
-
|
|
1368
|
+
if (result) {
|
|
1369
|
+
if (typeof result.text === 'string') {
|
|
1370
|
+
result.text = engine.restore(result.text, sessionTokenMap).restoredText;
|
|
1371
|
+
}
|
|
1372
|
+
if (typeof result.reasoning === 'string') {
|
|
1373
|
+
result.reasoning = engine.restore(result.reasoning, sessionTokenMap).restoredText;
|
|
1374
|
+
}
|
|
1375
|
+
if (Array.isArray(result.content)) {
|
|
1376
|
+
result.content = result.content.map(part => {
|
|
1377
|
+
if (part && (part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string') {
|
|
1378
|
+
return { ...part, text: engine.restore(part.text, sessionTokenMap).restoredText };
|
|
1379
|
+
}
|
|
1380
|
+
return part;
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1063
1383
|
}
|
|
1064
1384
|
return result;
|
|
1065
1385
|
};
|
|
@@ -1067,43 +1387,87 @@ function wrapAiModel(model, options = {}) {
|
|
|
1067
1387
|
|
|
1068
1388
|
if (typeof model.doStream === 'function') {
|
|
1069
1389
|
wrapped.doStream = async function(params) {
|
|
1070
|
-
const sessionTokenMap =
|
|
1390
|
+
const sessionTokenMap = createScopedTokenMap(params?.prompt);
|
|
1071
1391
|
const sanitizedParams = {
|
|
1072
1392
|
...params,
|
|
1073
1393
|
prompt: sanitizePromptParts(params.prompt, sessionTokenMap)
|
|
1074
1394
|
};
|
|
1395
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1075
1396
|
const result = await model.doStream(sanitizedParams);
|
|
1076
1397
|
if (result && result.stream) {
|
|
1077
1398
|
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1078
1399
|
const originalStream = result.stream;
|
|
1400
|
+
let usesV2Delta = false;
|
|
1079
1401
|
|
|
1080
1402
|
if (typeof originalStream.pipeThrough === 'function' && typeof TransformStream !== 'undefined') {
|
|
1081
1403
|
result.stream = originalStream.pipeThrough(new TransformStream({
|
|
1082
1404
|
transform(chunk, controller) {
|
|
1083
|
-
if (chunk && chunk.type === 'text-delta'
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1405
|
+
if (chunk && (chunk.type === 'text-delta' || chunk.type === 'reasoning' || chunk.type === 'reasoning-delta')) {
|
|
1406
|
+
if (typeof chunk.textDelta === 'string') {
|
|
1407
|
+
const restored = detok.feed(chunk.textDelta);
|
|
1408
|
+
if (restored) {
|
|
1409
|
+
controller.enqueue({
|
|
1410
|
+
...chunk,
|
|
1411
|
+
textDelta: restored,
|
|
1412
|
+
...(typeof chunk.delta === 'string' ? { delta: restored } : {})
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (typeof chunk.delta === 'string') {
|
|
1418
|
+
usesV2Delta = true;
|
|
1419
|
+
const restored = detok.feed(chunk.delta);
|
|
1420
|
+
if (restored) {
|
|
1421
|
+
controller.enqueue({ ...chunk, delta: restored, textDelta: restored });
|
|
1422
|
+
}
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1088
1425
|
}
|
|
1426
|
+
controller.enqueue(chunk);
|
|
1089
1427
|
},
|
|
1090
1428
|
flush(controller) {
|
|
1091
1429
|
const rem = detok.flush();
|
|
1092
|
-
if (rem)
|
|
1430
|
+
if (rem) {
|
|
1431
|
+
controller.enqueue(
|
|
1432
|
+
usesV2Delta
|
|
1433
|
+
? { type: 'text-delta', delta: rem, textDelta: rem }
|
|
1434
|
+
: { type: 'text-delta', textDelta: rem, delta: rem }
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1093
1437
|
}
|
|
1094
1438
|
}));
|
|
1095
1439
|
} else if (typeof originalStream[Symbol.asyncIterator] === 'function') {
|
|
1096
1440
|
async function* wrappedAsyncIterable() {
|
|
1097
1441
|
for await (const chunk of originalStream) {
|
|
1098
|
-
if (chunk && chunk.type === 'text-delta'
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1442
|
+
if (chunk && (chunk.type === 'text-delta' || chunk.type === 'reasoning' || chunk.type === 'reasoning-delta')) {
|
|
1443
|
+
if (typeof chunk.textDelta === 'string') {
|
|
1444
|
+
const restored = detok.feed(chunk.textDelta);
|
|
1445
|
+
if (restored) {
|
|
1446
|
+
yield {
|
|
1447
|
+
...chunk,
|
|
1448
|
+
textDelta: restored,
|
|
1449
|
+
...(typeof chunk.delta === 'string' ? { delta: restored } : {})
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
if (typeof chunk.delta === 'string') {
|
|
1455
|
+
usesV2Delta = true;
|
|
1456
|
+
const restored = detok.feed(chunk.delta);
|
|
1457
|
+
if (restored) {
|
|
1458
|
+
yield { ...chunk, delta: restored, textDelta: restored };
|
|
1459
|
+
}
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1103
1462
|
}
|
|
1463
|
+
yield chunk;
|
|
1104
1464
|
}
|
|
1105
1465
|
const rem = detok.flush();
|
|
1106
|
-
if (rem)
|
|
1466
|
+
if (rem) {
|
|
1467
|
+
yield usesV2Delta
|
|
1468
|
+
? { type: 'text-delta', delta: rem, textDelta: rem }
|
|
1469
|
+
: { type: 'text-delta', textDelta: rem, delta: rem };
|
|
1470
|
+
}
|
|
1107
1471
|
}
|
|
1108
1472
|
result.stream = wrappedAsyncIterable();
|
|
1109
1473
|
}
|
|
@@ -1117,6 +1481,631 @@ function wrapAiModel(model, options = {}) {
|
|
|
1117
1481
|
|
|
1118
1482
|
const wrapAiSdk = wrapAiModel;
|
|
1119
1483
|
|
|
1484
|
+
/**
|
|
1485
|
+
* Transparent Zero-Trust wrapper for Google Gemini SDKs:
|
|
1486
|
+
* - Official @google/genai SDK (GoogleGenAI instance with client.models.generateContent / generateContentStream)
|
|
1487
|
+
* - Legacy @google/generative-ai SDK (GenerativeModel or GoogleGenerativeAI instance)
|
|
1488
|
+
*
|
|
1489
|
+
* Automatically scrubs string, Part[], and Content[] prompts as well as systemInstruction in local RAM,
|
|
1490
|
+
* and restores masked tokens in both synchronous and streaming responses.
|
|
1491
|
+
*
|
|
1492
|
+
* @param {object} geminiClientOrModel - GoogleGenAI instance, GenerativeModel instance, or GoogleGenerativeAI client.
|
|
1493
|
+
* @param {object} [options={}] - PrivacyScrubber configuration options.
|
|
1494
|
+
* @returns {object} Wrapped Gemini client or model.
|
|
1495
|
+
*/
|
|
1496
|
+
function wrapGemini(geminiClientOrModel, options = {}) {
|
|
1497
|
+
if (!geminiClientOrModel || typeof geminiClientOrModel !== 'object') {
|
|
1498
|
+
throw new Error('[PrivacyScrubber] Invalid Gemini client or model passed to wrapGemini.');
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1502
|
+
const sharedSessionMap = {};
|
|
1503
|
+
|
|
1504
|
+
function sanitizeGeminiContentNode(node, sessionTokenMap) {
|
|
1505
|
+
if (node === null || node === undefined) return node;
|
|
1506
|
+
if (typeof node === 'string') {
|
|
1507
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1508
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1509
|
+
return res.scrubbedText;
|
|
1510
|
+
}
|
|
1511
|
+
if (Array.isArray(node)) {
|
|
1512
|
+
return node.map(item => sanitizeGeminiContentNode(item, sessionTokenMap));
|
|
1513
|
+
}
|
|
1514
|
+
if (typeof node === 'object') {
|
|
1515
|
+
const copy = { ...node };
|
|
1516
|
+
if (typeof copy.text === 'string') {
|
|
1517
|
+
const res = engine.sanitize(copy.text, { existingSessionMap: sessionTokenMap });
|
|
1518
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1519
|
+
copy.text = res.scrubbedText;
|
|
1520
|
+
}
|
|
1521
|
+
if (Array.isArray(copy.parts)) {
|
|
1522
|
+
copy.parts = copy.parts.map(p => sanitizeGeminiContentNode(p, sessionTokenMap));
|
|
1523
|
+
}
|
|
1524
|
+
if (copy.contents !== undefined) {
|
|
1525
|
+
copy.contents = sanitizeGeminiContentNode(copy.contents, sessionTokenMap);
|
|
1526
|
+
}
|
|
1527
|
+
if (copy.systemInstruction !== undefined) {
|
|
1528
|
+
copy.systemInstruction = sanitizeGeminiContentNode(copy.systemInstruction, sessionTokenMap);
|
|
1529
|
+
}
|
|
1530
|
+
if (copy.config && typeof copy.config === 'object') {
|
|
1531
|
+
copy.config = { ...copy.config };
|
|
1532
|
+
if (copy.config.systemInstruction !== undefined) {
|
|
1533
|
+
copy.config.systemInstruction = sanitizeGeminiContentNode(copy.config.systemInstruction, sessionTokenMap);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return copy;
|
|
1537
|
+
}
|
|
1538
|
+
return node;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function restoreGeminiResponseObject(respObj, sessionTokenMap, detok = null) {
|
|
1542
|
+
if (!respObj || typeof respObj !== 'object') return respObj;
|
|
1543
|
+
const restoreFn = (str) => detok ? detok.feed(str) : engine.restore(str, sessionTokenMap).restoredText;
|
|
1544
|
+
|
|
1545
|
+
let combinedText = '';
|
|
1546
|
+
if (Array.isArray(respObj.candidates)) {
|
|
1547
|
+
respObj.candidates = respObj.candidates.map(cand => {
|
|
1548
|
+
if (!cand || typeof cand !== 'object') return cand;
|
|
1549
|
+
const candCopy = { ...cand };
|
|
1550
|
+
if (candCopy.content && Array.isArray(candCopy.content.parts)) {
|
|
1551
|
+
candCopy.content = {
|
|
1552
|
+
...candCopy.content,
|
|
1553
|
+
parts: candCopy.content.parts.map(part => {
|
|
1554
|
+
if (part && typeof part.text === 'string') {
|
|
1555
|
+
const restoredText = restoreFn(part.text);
|
|
1556
|
+
combinedText += restoredText;
|
|
1557
|
+
return { ...part, text: restoredText };
|
|
1558
|
+
}
|
|
1559
|
+
return part;
|
|
1560
|
+
})
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
return candCopy;
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (typeof respObj.text === 'function') {
|
|
1568
|
+
const origTextFn = respObj.text.bind(respObj);
|
|
1569
|
+
respObj.text = () => {
|
|
1570
|
+
if (combinedText) return combinedText;
|
|
1571
|
+
const raw = origTextFn();
|
|
1572
|
+
return typeof raw === 'string' ? restoreFn(raw) : raw;
|
|
1573
|
+
};
|
|
1574
|
+
} else if (typeof respObj.text === 'string') {
|
|
1575
|
+
const restored = combinedText || restoreFn(respObj.text);
|
|
1576
|
+
try {
|
|
1577
|
+
Object.defineProperty(respObj, 'text', {
|
|
1578
|
+
value: restored,
|
|
1579
|
+
writable: true,
|
|
1580
|
+
enumerable: true,
|
|
1581
|
+
configurable: true
|
|
1582
|
+
});
|
|
1583
|
+
} catch (_) {
|
|
1584
|
+
respObj.text = restored;
|
|
1585
|
+
}
|
|
1586
|
+
} else if (combinedText) {
|
|
1587
|
+
try {
|
|
1588
|
+
Object.defineProperty(respObj, 'text', {
|
|
1589
|
+
value: combinedText,
|
|
1590
|
+
writable: true,
|
|
1591
|
+
enumerable: true,
|
|
1592
|
+
configurable: true
|
|
1593
|
+
});
|
|
1594
|
+
} catch (_) {}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (respObj.response && typeof respObj.response === 'object') {
|
|
1598
|
+
restoreGeminiResponseObject(respObj.response, sessionTokenMap, detok);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
return respObj;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function wrapGeminiStreamResult(streamResult, sessionTokenMap) {
|
|
1605
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1606
|
+
|
|
1607
|
+
async function* wrapAsyncChunks(iterable) {
|
|
1608
|
+
let lastChunkTemplate = null;
|
|
1609
|
+
for await (const chunk of iterable) {
|
|
1610
|
+
if (!chunk) continue;
|
|
1611
|
+
lastChunkTemplate = chunk;
|
|
1612
|
+
if (typeof chunk === 'string') {
|
|
1613
|
+
const out = detok.feed(chunk);
|
|
1614
|
+
if (out) yield out;
|
|
1615
|
+
} else if (typeof chunk === 'object') {
|
|
1616
|
+
const cloned = Object.create(Object.getPrototypeOf(chunk));
|
|
1617
|
+
Object.assign(cloned, chunk);
|
|
1618
|
+
restoreGeminiResponseObject(cloned, sessionTokenMap, detok);
|
|
1619
|
+
yield cloned;
|
|
1620
|
+
} else {
|
|
1621
|
+
yield chunk;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
const tail = detok.flush();
|
|
1625
|
+
if (tail) {
|
|
1626
|
+
if (lastChunkTemplate && typeof lastChunkTemplate === 'object') {
|
|
1627
|
+
const finalChunk = {
|
|
1628
|
+
...lastChunkTemplate,
|
|
1629
|
+
candidates: [{ content: { role: 'model', parts: [{ text: tail }] } }],
|
|
1630
|
+
text: tail
|
|
1631
|
+
};
|
|
1632
|
+
if (typeof lastChunkTemplate.text === 'function') {
|
|
1633
|
+
finalChunk.text = () => tail;
|
|
1634
|
+
}
|
|
1635
|
+
yield finalChunk;
|
|
1636
|
+
} else {
|
|
1637
|
+
yield tail;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (streamResult && typeof streamResult[Symbol.asyncIterator] === 'function') {
|
|
1643
|
+
return wrapAsyncChunks(streamResult);
|
|
1644
|
+
}
|
|
1645
|
+
if (streamResult && streamResult.stream && typeof streamResult.stream[Symbol.asyncIterator] === 'function') {
|
|
1646
|
+
const wrappedStream = wrapAsyncChunks(streamResult.stream);
|
|
1647
|
+
const wrappedResponse = streamResult.response && typeof streamResult.response.then === 'function'
|
|
1648
|
+
? streamResult.response.then(r => restoreGeminiResponseObject(r, sessionTokenMap))
|
|
1649
|
+
: streamResult.response;
|
|
1650
|
+
return {
|
|
1651
|
+
...streamResult,
|
|
1652
|
+
stream: wrappedStream,
|
|
1653
|
+
response: wrappedResponse
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
return streamResult;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function wrapTargetObject(target) {
|
|
1660
|
+
if (typeof target.generateContent === 'function') {
|
|
1661
|
+
const origGen = target.generateContent.bind(target);
|
|
1662
|
+
target.generateContent = async function(req, ...rest) {
|
|
1663
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1664
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1665
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1666
|
+
const res = await origGen(sanitizedReq, ...rest);
|
|
1667
|
+
return restoreGeminiResponseObject(res, sessionTokenMap);
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
if (typeof target.generateContentStream === 'function') {
|
|
1671
|
+
const origStream = target.generateContentStream.bind(target);
|
|
1672
|
+
target.generateContentStream = async function(req, ...rest) {
|
|
1673
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1674
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1675
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1676
|
+
const streamRes = await origStream(sanitizedReq, ...rest);
|
|
1677
|
+
return wrapGeminiStreamResult(streamRes, sessionTokenMap);
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
if (geminiClientOrModel.models && typeof geminiClientOrModel.models === 'object') {
|
|
1683
|
+
wrapTargetObject(geminiClientOrModel.models);
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof geminiClientOrModel.generateContent === 'function' || typeof geminiClientOrModel.generateContentStream === 'function') {
|
|
1686
|
+
wrapTargetObject(geminiClientOrModel);
|
|
1687
|
+
}
|
|
1688
|
+
if (typeof geminiClientOrModel.getGenerativeModel === 'function') {
|
|
1689
|
+
const origGetModel = geminiClientOrModel.getGenerativeModel.bind(geminiClientOrModel);
|
|
1690
|
+
geminiClientOrModel.getGenerativeModel = function(modelParams, ...rest) {
|
|
1691
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1692
|
+
const sanitizedParams = sanitizeGeminiContentNode(modelParams, sessionTokenMap);
|
|
1693
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1694
|
+
const modelInstance = origGetModel(sanitizedParams, ...rest);
|
|
1695
|
+
if (modelInstance && typeof modelInstance === 'object') {
|
|
1696
|
+
wrapTargetObject(modelInstance);
|
|
1697
|
+
}
|
|
1698
|
+
return modelInstance;
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
geminiClientOrModel._ztdsEngine = engine;
|
|
1703
|
+
return geminiClientOrModel;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const wrapGoogleGenAI = wrapGemini;
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Creates a universal Zero-Trust HTTP/SSE `fetch` wrapper for any LLM provider or SDK
|
|
1710
|
+
* (OpenAI, Anthropic, OpenRouter, Groq, DeepSeek, Mistral, Together, Ollama, Cloudflare AI Gateway).
|
|
1711
|
+
*
|
|
1712
|
+
* Automatically sanitizes outbound JSON request bodies (`messages`, `prompt`, `input`, `contents`, `system`)
|
|
1713
|
+
* in local RAM before network transmission, and transparently de-tokenizes both JSON responses
|
|
1714
|
+
* and Server-Sent Events (`text/event-stream`) streams in real time.
|
|
1715
|
+
*
|
|
1716
|
+
* Usage:
|
|
1717
|
+
* const client = new OpenAI({ fetch: createPrivacyFetch({ profile: 'General' }) });
|
|
1718
|
+
*
|
|
1719
|
+
* @param {object} [options={}] - Configuration options.
|
|
1720
|
+
* @returns {Function} Drop-in `fetch(input, init)` replacement.
|
|
1721
|
+
*/
|
|
1722
|
+
function createPrivacyFetch(options = {}) {
|
|
1723
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1724
|
+
const baseFetch = options.fetch || (typeof globalThis !== 'undefined' && globalThis.fetch);
|
|
1725
|
+
const sanitizeInputs = options.sanitizeInputs !== false;
|
|
1726
|
+
const sanitizeOutputs = options.sanitizeOutputs !== false;
|
|
1727
|
+
|
|
1728
|
+
const PROMPT_KEYS = new Set([
|
|
1729
|
+
'messages', 'prompt', 'input', 'contents', 'system',
|
|
1730
|
+
'systemInstruction', 'system_instruction', 'instructions', 'text', 'content'
|
|
1731
|
+
]);
|
|
1732
|
+
const SAFE_SKIP_KEYS = new Set([
|
|
1733
|
+
'model', 'role', 'type', 'id', 'object', 'finish_reason', 'stop_reason',
|
|
1734
|
+
'encoding_format', 'response_format', 'tool_choice', 'stream'
|
|
1735
|
+
]);
|
|
1736
|
+
|
|
1737
|
+
function sanitizeJsonNode(node, sessionTokenMap) {
|
|
1738
|
+
if (node === null || node === undefined) return node;
|
|
1739
|
+
if (typeof node === 'string') {
|
|
1740
|
+
if (!node.trim()) return node;
|
|
1741
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1742
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1743
|
+
return res.scrubbedText;
|
|
1744
|
+
}
|
|
1745
|
+
if (Array.isArray(node)) {
|
|
1746
|
+
return node.map(item => sanitizeJsonNode(item, sessionTokenMap));
|
|
1747
|
+
}
|
|
1748
|
+
if (typeof node === 'object') {
|
|
1749
|
+
const out = {};
|
|
1750
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1751
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
1752
|
+
if (SAFE_SKIP_KEYS.has(k)) {
|
|
1753
|
+
out[k] = v;
|
|
1754
|
+
} else if (PROMPT_KEYS.has(k) || typeof v === 'object') {
|
|
1755
|
+
out[k] = sanitizeJsonNode(v, sessionTokenMap);
|
|
1756
|
+
} else {
|
|
1757
|
+
out[k] = v;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return out;
|
|
1761
|
+
}
|
|
1762
|
+
return node;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function detokenizeSseJsonPayload(parsed, detok) {
|
|
1766
|
+
if (!parsed || typeof parsed !== 'object') return parsed;
|
|
1767
|
+
if (Array.isArray(parsed.choices)) {
|
|
1768
|
+
parsed.choices = parsed.choices.map(choice => {
|
|
1769
|
+
if (!choice || typeof choice !== 'object') return choice;
|
|
1770
|
+
const c = { ...choice };
|
|
1771
|
+
if (c.delta && typeof c.delta === 'object') {
|
|
1772
|
+
c.delta = { ...c.delta };
|
|
1773
|
+
if (typeof c.delta.content === 'string') {
|
|
1774
|
+
c.delta.content = detok.feed(c.delta.content);
|
|
1775
|
+
}
|
|
1776
|
+
if (typeof c.delta.reasoning_content === 'string') {
|
|
1777
|
+
c.delta.reasoning_content = detok.feed(c.delta.reasoning_content);
|
|
1778
|
+
}
|
|
1779
|
+
if (typeof c.delta.text === 'string') {
|
|
1780
|
+
c.delta.text = detok.feed(c.delta.text);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (typeof c.text === 'string') {
|
|
1784
|
+
c.text = detok.feed(c.text);
|
|
1785
|
+
}
|
|
1786
|
+
return c;
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
if (parsed.delta && typeof parsed.delta === 'object') {
|
|
1790
|
+
parsed.delta = { ...parsed.delta };
|
|
1791
|
+
if (typeof parsed.delta.text === 'string') {
|
|
1792
|
+
parsed.delta.text = detok.feed(parsed.delta.text);
|
|
1793
|
+
}
|
|
1794
|
+
if (typeof parsed.delta.partial_json === 'string') {
|
|
1795
|
+
parsed.delta.partial_json = detok.feed(parsed.delta.partial_json);
|
|
1796
|
+
}
|
|
1797
|
+
} else if (typeof parsed.delta === 'string') {
|
|
1798
|
+
parsed.delta = detok.feed(parsed.delta);
|
|
1799
|
+
}
|
|
1800
|
+
if (Array.isArray(parsed.candidates)) {
|
|
1801
|
+
parsed.candidates = parsed.candidates.map(cand => {
|
|
1802
|
+
if (cand && cand.content && Array.isArray(cand.content.parts)) {
|
|
1803
|
+
return {
|
|
1804
|
+
...cand,
|
|
1805
|
+
content: {
|
|
1806
|
+
...cand.content,
|
|
1807
|
+
parts: cand.content.parts.map(p =>
|
|
1808
|
+
(p && typeof p.text === 'string') ? { ...p, text: detok.feed(p.text) } : p
|
|
1809
|
+
)
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
return cand;
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
if (typeof parsed.response === 'string') {
|
|
1817
|
+
parsed.response = detok.feed(parsed.response);
|
|
1818
|
+
}
|
|
1819
|
+
return parsed;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const privacyFetch = async function(input, init = {}) {
|
|
1823
|
+
if (typeof baseFetch !== 'function') {
|
|
1824
|
+
throw new Error('[PrivacyScrubber] No global fetch implementation found; pass { fetch } to createPrivacyFetch.');
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
1828
|
+
let modifiedInit = init ? { ...init } : {};
|
|
1829
|
+
|
|
1830
|
+
if (sanitizeInputs && modifiedInit && typeof modifiedInit.body === 'string') {
|
|
1831
|
+
const trimmed = modifiedInit.body.trim();
|
|
1832
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
1833
|
+
try {
|
|
1834
|
+
const parsed = JSON.parse(modifiedInit.body);
|
|
1835
|
+
const sanitizedBody = sanitizeJsonNode(parsed, sessionTokenMap);
|
|
1836
|
+
modifiedInit.body = JSON.stringify(sanitizedBody);
|
|
1837
|
+
if (modifiedInit.headers) {
|
|
1838
|
+
if (typeof Headers !== 'undefined' && modifiedInit.headers instanceof Headers) {
|
|
1839
|
+
const h = new Headers(modifiedInit.headers);
|
|
1840
|
+
h.delete('content-length');
|
|
1841
|
+
modifiedInit.headers = h;
|
|
1842
|
+
} else if (typeof modifiedInit.headers === 'object' && !Array.isArray(modifiedInit.headers)) {
|
|
1843
|
+
const h = { ...modifiedInit.headers };
|
|
1844
|
+
delete h['content-length'];
|
|
1845
|
+
delete h['Content-Length'];
|
|
1846
|
+
modifiedInit.headers = h;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (typeof options.onSanitized === 'function') {
|
|
1850
|
+
try {
|
|
1851
|
+
options.onSanitized({
|
|
1852
|
+
url: typeof input === 'string' ? input : (input && input.url) || '',
|
|
1853
|
+
tokenMap: sessionTokenMap,
|
|
1854
|
+
tokenCount: Object.keys(sessionTokenMap).length
|
|
1855
|
+
});
|
|
1856
|
+
} catch (_) {}
|
|
1857
|
+
}
|
|
1858
|
+
} catch (_) {}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
const response = await baseFetch(input, modifiedInit);
|
|
1863
|
+
if (!sanitizeOutputs || !response || Object.keys(sessionTokenMap).length === 0) {
|
|
1864
|
+
return response;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const contentType = (response.headers && typeof response.headers.get === 'function')
|
|
1868
|
+
? (response.headers.get('content-type') || '').toLowerCase()
|
|
1869
|
+
: '';
|
|
1870
|
+
|
|
1871
|
+
if (contentType.includes('text/event-stream') && response.body && typeof response.body.pipeThrough === 'function' && typeof TransformStream !== 'undefined') {
|
|
1872
|
+
const detok = new StreamingDetokenizer(sessionTokenMap, options.streamingOptions);
|
|
1873
|
+
const decoder = new TextDecoder();
|
|
1874
|
+
const encoder = new TextEncoder();
|
|
1875
|
+
let lineBuffer = '';
|
|
1876
|
+
|
|
1877
|
+
const processSseLine = (line) => {
|
|
1878
|
+
if (!line.startsWith('data:')) return line;
|
|
1879
|
+
const dataPayload = line.slice(5).trim();
|
|
1880
|
+
if (!dataPayload || dataPayload === '[DONE]') {
|
|
1881
|
+
const rem = detok.flush();
|
|
1882
|
+
return rem ? `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: rem } }] })}\n\n${line}` : line;
|
|
1883
|
+
}
|
|
1884
|
+
if (dataPayload.startsWith('{')) {
|
|
1885
|
+
try {
|
|
1886
|
+
const parsed = JSON.parse(dataPayload);
|
|
1887
|
+
const restoredObj = detokenizeSseJsonPayload(parsed, detok);
|
|
1888
|
+
return `data: ${JSON.stringify(restoredObj)}`;
|
|
1889
|
+
} catch (_) {
|
|
1890
|
+
return `data: ${detok.feed(dataPayload)}`;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return `data: ${detok.feed(dataPayload)}`;
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
const transformedBody = response.body.pipeThrough(new TransformStream({
|
|
1897
|
+
transform(chunk, controller) {
|
|
1898
|
+
const str = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
1899
|
+
lineBuffer += str;
|
|
1900
|
+
const lines = lineBuffer.split(/\r?\n/);
|
|
1901
|
+
lineBuffer = lines.pop() || '';
|
|
1902
|
+
if (lines.length > 0) {
|
|
1903
|
+
const outLines = lines.map(processSseLine).join('\n') + '\n';
|
|
1904
|
+
controller.enqueue(encoder.encode(outLines));
|
|
1905
|
+
}
|
|
1906
|
+
},
|
|
1907
|
+
flush(controller) {
|
|
1908
|
+
let finalOut = '';
|
|
1909
|
+
if (lineBuffer) {
|
|
1910
|
+
finalOut += processSseLine(lineBuffer) + '\n';
|
|
1911
|
+
lineBuffer = '';
|
|
1912
|
+
}
|
|
1913
|
+
const rem = detok.flush();
|
|
1914
|
+
if (rem) {
|
|
1915
|
+
finalOut += `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: rem } }] })}\n\n`;
|
|
1916
|
+
}
|
|
1917
|
+
if (finalOut) {
|
|
1918
|
+
controller.enqueue(encoder.encode(finalOut));
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}));
|
|
1922
|
+
|
|
1923
|
+
if (typeof Response !== 'undefined') {
|
|
1924
|
+
return new Response(transformedBody, {
|
|
1925
|
+
status: response.status,
|
|
1926
|
+
statusText: response.statusText,
|
|
1927
|
+
headers: response.headers
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
return { ...response, body: transformedBody };
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (contentType.includes('application/json') || typeof response.json === 'function') {
|
|
1934
|
+
try {
|
|
1935
|
+
const rawText = typeof response.text === 'function'
|
|
1936
|
+
? await response.text()
|
|
1937
|
+
: JSON.stringify(await response.json());
|
|
1938
|
+
const parsed = JSON.parse(rawText);
|
|
1939
|
+
const restoredObj = restoreObject(parsed, sessionTokenMap);
|
|
1940
|
+
const restoredText = JSON.stringify(restoredObj);
|
|
1941
|
+
if (typeof Response !== 'undefined') {
|
|
1942
|
+
const headers = new Headers(response.headers || {});
|
|
1943
|
+
headers.delete('content-length');
|
|
1944
|
+
return new Response(restoredText, {
|
|
1945
|
+
status: response.status,
|
|
1946
|
+
statusText: response.statusText,
|
|
1947
|
+
headers
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
...response,
|
|
1952
|
+
text: async () => restoredText,
|
|
1953
|
+
json: async () => restoredObj
|
|
1954
|
+
};
|
|
1955
|
+
} catch (_) {
|
|
1956
|
+
return response;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
return response;
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
privacyFetch.engine = engine;
|
|
1964
|
+
return privacyFetch;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Creates OpenAI Agents SDK (@openai/agents) compatible InputGuardrail and OutputGuardrail objects.
|
|
1969
|
+
*
|
|
1970
|
+
* Supports two operating modes:
|
|
1971
|
+
* 1. Sanitization & Telemetry Mode (default): Scrubs PII/secrets into engine.sessionMap and attaches
|
|
1972
|
+
* CISO compliance telemetry to `outputInfo` while allowing execution (`tripwireTriggered: false`).
|
|
1973
|
+
* 2. Strict Tripwire Mode (`tripwireOnCritical: true` or `tripwireOnAnyPii: true`): Triggers the
|
|
1974
|
+
* OpenAI Agents SDK guardrail tripwire (`tripwireTriggered: true`) to halt agent execution when
|
|
1975
|
+
* critical secrets/PII or any PII are detected.
|
|
1976
|
+
*
|
|
1977
|
+
* @param {object} [options={}] - Configuration options.
|
|
1978
|
+
* @returns {object} `{ inputGuardrail, outputGuardrail, inputGuardrails, outputGuardrails, engine }`
|
|
1979
|
+
*/
|
|
1980
|
+
function createOpenAIAgentGuardrails(options = {}) {
|
|
1981
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1982
|
+
const tripwireOnCritical = options.tripwireOnCritical === true;
|
|
1983
|
+
const tripwireOnAnyPii = options.tripwireOnAnyPii === true;
|
|
1984
|
+
|
|
1985
|
+
function extractAndScrubAgentInput(input, sessionTokenMap) {
|
|
1986
|
+
if (typeof input === 'string') {
|
|
1987
|
+
const res = engine.sanitize(input, { existingSessionMap: sessionTokenMap });
|
|
1988
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1989
|
+
return { scrubbedInput: res.scrubbedText, count: res.count };
|
|
1990
|
+
}
|
|
1991
|
+
if (Array.isArray(input)) {
|
|
1992
|
+
let totalCount = 0;
|
|
1993
|
+
const scrubbedList = input.map(item => {
|
|
1994
|
+
if (typeof item === 'string') {
|
|
1995
|
+
const r = engine.sanitize(item, { existingSessionMap: sessionTokenMap });
|
|
1996
|
+
Object.assign(sessionTokenMap, r.tokenMap);
|
|
1997
|
+
totalCount += r.count;
|
|
1998
|
+
return r.scrubbedText;
|
|
1999
|
+
}
|
|
2000
|
+
if (item && typeof item === 'object') {
|
|
2001
|
+
const copy = { ...item };
|
|
2002
|
+
if (typeof copy.content === 'string') {
|
|
2003
|
+
const r = engine.sanitize(copy.content, { existingSessionMap: sessionTokenMap });
|
|
2004
|
+
Object.assign(sessionTokenMap, r.tokenMap);
|
|
2005
|
+
totalCount += r.count;
|
|
2006
|
+
copy.content = r.scrubbedText;
|
|
2007
|
+
if (options.mutateInPlace !== false) item.content = r.scrubbedText;
|
|
2008
|
+
} else if (Array.isArray(copy.content)) {
|
|
2009
|
+
copy.content = copy.content.map(part => {
|
|
2010
|
+
if (part && typeof part.text === 'string') {
|
|
2011
|
+
const r = engine.sanitize(part.text, { existingSessionMap: sessionTokenMap });
|
|
2012
|
+
Object.assign(sessionTokenMap, r.tokenMap);
|
|
2013
|
+
totalCount += r.count;
|
|
2014
|
+
if (options.mutateInPlace !== false) part.text = r.scrubbedText;
|
|
2015
|
+
return { ...part, text: r.scrubbedText };
|
|
2016
|
+
}
|
|
2017
|
+
return part;
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
return copy;
|
|
2021
|
+
}
|
|
2022
|
+
return item;
|
|
2023
|
+
});
|
|
2024
|
+
return { scrubbedInput: scrubbedList, count: totalCount };
|
|
2025
|
+
}
|
|
2026
|
+
return { scrubbedInput: input, count: 0 };
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const inputGuardrail = {
|
|
2030
|
+
name: options.inputGuardrailName || 'PrivacyScrubber ZTDS Input Guardrail',
|
|
2031
|
+
async execute({ input, context } = {}) {
|
|
2032
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
2033
|
+
const { scrubbedInput, count } = extractAndScrubAgentInput(input, sessionTokenMap);
|
|
2034
|
+
Object.assign(engine.sessionMap, sessionTokenMap);
|
|
2035
|
+
|
|
2036
|
+
if (context && typeof context === 'object') {
|
|
2037
|
+
context.ztdsTokenMap = engine.sessionMap;
|
|
2038
|
+
context.ztdsScrubbedInput = scrubbedInput;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const telemetry = buildAuditTelemetry(sessionTokenMap);
|
|
2042
|
+
const isCritical = telemetry.riskLevel && telemetry.riskLevel.includes('CRITICAL');
|
|
2043
|
+
const tripwireTriggered = Boolean(
|
|
2044
|
+
(tripwireOnAnyPii && count > 0) ||
|
|
2045
|
+
(tripwireOnCritical && count > 0 && isCritical)
|
|
2046
|
+
);
|
|
2047
|
+
|
|
2048
|
+
return {
|
|
2049
|
+
tripwireTriggered,
|
|
2050
|
+
outputInfo: {
|
|
2051
|
+
scrubbedInput,
|
|
2052
|
+
tokenMap: { ...sessionTokenMap },
|
|
2053
|
+
tokenCount: count,
|
|
2054
|
+
riskLevel: telemetry.riskLevel,
|
|
2055
|
+
frameworks: telemetry.frameworksList,
|
|
2056
|
+
telemetry
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
const outputGuardrail = {
|
|
2063
|
+
name: options.outputGuardrailName || 'PrivacyScrubber ZTDS Output Guardrail',
|
|
2064
|
+
async execute({ agentOutput, context } = {}) {
|
|
2065
|
+
const activeMap = (context && context.ztdsTokenMap) || engine.sessionMap || {};
|
|
2066
|
+
let restoredOutput = agentOutput;
|
|
2067
|
+
let restoredCount = 0;
|
|
2068
|
+
|
|
2069
|
+
if (typeof agentOutput === 'string') {
|
|
2070
|
+
const res = engine.restore(agentOutput, activeMap);
|
|
2071
|
+
restoredOutput = res.restoredText;
|
|
2072
|
+
restoredCount = res.restoredCount;
|
|
2073
|
+
} else if (agentOutput && typeof agentOutput === 'object') {
|
|
2074
|
+
restoredOutput = restoreObject(agentOutput, activeMap);
|
|
2075
|
+
if (options.mutateInPlace !== false) {
|
|
2076
|
+
Object.assign(agentOutput, restoredOutput);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
if (context && typeof context === 'object') {
|
|
2081
|
+
context.ztdsRestoredOutput = restoredOutput;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
let leakDetected = false;
|
|
2085
|
+
if (options.tripwireOnOutputLeak === true && typeof restoredOutput === 'string') {
|
|
2086
|
+
const check = engine.sanitize(restoredOutput, { persistSession: false });
|
|
2087
|
+
leakDetected = check.count > 0;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
return {
|
|
2091
|
+
tripwireTriggered: leakDetected,
|
|
2092
|
+
outputInfo: {
|
|
2093
|
+
restoredOutput,
|
|
2094
|
+
restoredCount
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
return {
|
|
2101
|
+
inputGuardrail,
|
|
2102
|
+
outputGuardrail,
|
|
2103
|
+
inputGuardrails: [inputGuardrail],
|
|
2104
|
+
outputGuardrails: [outputGuardrail],
|
|
2105
|
+
engine
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1120
2109
|
/**
|
|
1121
2110
|
* Framework middleware helper for LangChain / LlamaIndex pipelines.
|
|
1122
2111
|
*/
|
|
@@ -1756,9 +2745,11 @@ class StreamingDetokenizer {
|
|
|
1756
2745
|
*/
|
|
1757
2746
|
async* wrapStream(asyncIterable) {
|
|
1758
2747
|
let lastOpenAIChunk = null;
|
|
2748
|
+
let lastResponsesDeltaChunk = null;
|
|
2749
|
+
let lastAnthropicDeltaChunk = null;
|
|
1759
2750
|
for await (const chunk of asyncIterable) {
|
|
1760
2751
|
if (chunk === null || chunk === undefined) continue;
|
|
1761
|
-
// Handle OpenAI chunk object
|
|
2752
|
+
// Handle OpenAI chat.completions chunk object
|
|
1762
2753
|
if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
|
|
1763
2754
|
lastOpenAIChunk = chunk;
|
|
1764
2755
|
const delta = chunk.choices[0].delta.content;
|
|
@@ -1776,6 +2767,55 @@ class StreamingDetokenizer {
|
|
|
1776
2767
|
continue;
|
|
1777
2768
|
}
|
|
1778
2769
|
|
|
2770
|
+
// Handle OpenAI Responses API streaming events (response.output_text.delta / done)
|
|
2771
|
+
if (chunk && typeof chunk === 'object' && typeof chunk.type === 'string' && chunk.type.startsWith('response.')) {
|
|
2772
|
+
if (chunk.type === 'response.output_text.delta' && typeof chunk.delta === 'string') {
|
|
2773
|
+
lastResponsesDeltaChunk = chunk;
|
|
2774
|
+
const restored = this.feed(chunk.delta);
|
|
2775
|
+
if (restored) {
|
|
2776
|
+
yield { ...chunk, delta: restored };
|
|
2777
|
+
}
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
if (chunk.type === 'response.output_text.done' && typeof chunk.text === 'string') {
|
|
2781
|
+
const remaining = this.flush();
|
|
2782
|
+
if (remaining && lastResponsesDeltaChunk) {
|
|
2783
|
+
yield { ...lastResponsesDeltaChunk, delta: remaining };
|
|
2784
|
+
}
|
|
2785
|
+
yield { ...chunk, text: this.replacer(chunk.text) };
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
yield chunk;
|
|
2789
|
+
continue;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// Handle Anthropic messages.create({ stream: true }) events (content_block_delta / content_block_stop)
|
|
2793
|
+
if (chunk && typeof chunk === 'object' && typeof chunk.type === 'string' && (
|
|
2794
|
+
chunk.type === 'content_block_delta' ||
|
|
2795
|
+
chunk.type === 'content_block_start' ||
|
|
2796
|
+
chunk.type === 'content_block_stop' ||
|
|
2797
|
+
chunk.type === 'message_start' ||
|
|
2798
|
+
chunk.type === 'message_delta' ||
|
|
2799
|
+
chunk.type === 'message_stop'
|
|
2800
|
+
)) {
|
|
2801
|
+
if (chunk.type === 'content_block_delta' && chunk.delta && chunk.delta.type === 'text_delta' && typeof chunk.delta.text === 'string') {
|
|
2802
|
+
lastAnthropicDeltaChunk = chunk;
|
|
2803
|
+
const restored = this.feed(chunk.delta.text);
|
|
2804
|
+
if (restored) {
|
|
2805
|
+
yield { ...chunk, delta: { ...chunk.delta, text: restored } };
|
|
2806
|
+
}
|
|
2807
|
+
continue;
|
|
2808
|
+
}
|
|
2809
|
+
if ((chunk.type === 'content_block_stop' || chunk.type === 'message_stop') && this.pendingBuffer) {
|
|
2810
|
+
const remaining = this.flush();
|
|
2811
|
+
if (remaining && lastAnthropicDeltaChunk) {
|
|
2812
|
+
yield { ...lastAnthropicDeltaChunk, delta: { ...lastAnthropicDeltaChunk.delta, text: remaining } };
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
yield chunk;
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
1779
2819
|
let text = '';
|
|
1780
2820
|
if (typeof chunk === 'string') {
|
|
1781
2821
|
text = chunk;
|
|
@@ -1800,6 +2840,10 @@ class StreamingDetokenizer {
|
|
|
1800
2840
|
idx === 0 ? { ...c, delta: { ...c.delta, content: finalChunk } } : c
|
|
1801
2841
|
);
|
|
1802
2842
|
yield { ...lastOpenAIChunk, choices: clonedChoices };
|
|
2843
|
+
} else if (lastResponsesDeltaChunk) {
|
|
2844
|
+
yield { ...lastResponsesDeltaChunk, delta: finalChunk };
|
|
2845
|
+
} else if (lastAnthropicDeltaChunk) {
|
|
2846
|
+
yield { ...lastAnthropicDeltaChunk, delta: { ...lastAnthropicDeltaChunk.delta, text: finalChunk } };
|
|
1803
2847
|
} else {
|
|
1804
2848
|
yield finalChunk;
|
|
1805
2849
|
}
|
|
@@ -1961,7 +3005,7 @@ function formatCefEvent(eventData = {}) {
|
|
|
1961
3005
|
const {
|
|
1962
3006
|
vendor = 'PrivacyScrubber',
|
|
1963
3007
|
product = 'ZTDS',
|
|
1964
|
-
version =
|
|
3008
|
+
version = SDK_VERSION,
|
|
1965
3009
|
eventClassId = 'PII_REDACTION',
|
|
1966
3010
|
eventName = 'Zero-Trust PII Sanitization Executed',
|
|
1967
3011
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -2030,7 +3074,7 @@ function formatSyslogEvent(eventData = {}) {
|
|
|
2030
3074
|
function formatJsonlEvent(eventData = {}) {
|
|
2031
3075
|
const {
|
|
2032
3076
|
timestamp = new Date().toISOString(),
|
|
2033
|
-
version =
|
|
3077
|
+
version = SDK_VERSION,
|
|
2034
3078
|
protocol = 'ZTDS',
|
|
2035
3079
|
event = 'sanitization',
|
|
2036
3080
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -2078,12 +3122,22 @@ function createAuditLogger(options = {}) {
|
|
|
2078
3122
|
const buffer = [];
|
|
2079
3123
|
|
|
2080
3124
|
if (typeof destination === 'string' && destination.trim()) {
|
|
3125
|
+
if (destination.includes('\0')) {
|
|
3126
|
+
throw new Error('[PrivacyScrubber AuditLogger] Security violation: null byte in destination path.');
|
|
3127
|
+
}
|
|
3128
|
+
const resolvedDest = path.resolve(destination.trim());
|
|
3129
|
+
if (fs.existsSync(resolvedDest)) {
|
|
3130
|
+
const st = fs.lstatSync(resolvedDest);
|
|
3131
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
3132
|
+
throw new Error('[PrivacyScrubber AuditLogger] Security violation: destination must be a regular non-symlink file.');
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
2081
3135
|
try {
|
|
2082
|
-
const dir = path.dirname(
|
|
3136
|
+
const dir = path.dirname(resolvedDest);
|
|
2083
3137
|
if (!fs.existsSync(dir)) {
|
|
2084
3138
|
fs.mkdirSync(dir, { recursive: true });
|
|
2085
3139
|
}
|
|
2086
|
-
fileHandle =
|
|
3140
|
+
fileHandle = resolvedDest;
|
|
2087
3141
|
} catch {}
|
|
2088
3142
|
} else if (destination && typeof destination.write === 'function') {
|
|
2089
3143
|
streamHandle = destination;
|
|
@@ -2113,6 +3167,12 @@ function createAuditLogger(options = {}) {
|
|
|
2113
3167
|
buffer.push(line);
|
|
2114
3168
|
if (fileHandle) {
|
|
2115
3169
|
try {
|
|
3170
|
+
if (fs.existsSync(fileHandle)) {
|
|
3171
|
+
const st = fs.lstatSync(fileHandle);
|
|
3172
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
3173
|
+
throw new Error('Target destination became a symlink or non-regular file.');
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
2116
3176
|
fs.appendFileSync(fileHandle, line + '\n', 'utf8');
|
|
2117
3177
|
} catch (e) {
|
|
2118
3178
|
console.error('[PrivacyScrubber AuditLogger] File write error:', e);
|
|
@@ -2416,11 +3476,41 @@ function applyPatch(filePath, content, options = {}) {
|
|
|
2416
3476
|
const {
|
|
2417
3477
|
tokenMap = null,
|
|
2418
3478
|
engine = null,
|
|
2419
|
-
createBackup = true
|
|
3479
|
+
createBackup = true,
|
|
3480
|
+
cwd = process.cwd(),
|
|
3481
|
+
allowOutsideWorkspace = false
|
|
2420
3482
|
} = options;
|
|
2421
3483
|
|
|
3484
|
+
if (filePath.includes('\0')) {
|
|
3485
|
+
throw new Error('[applyPatch] Security policy violation — null byte in filePath blocked.');
|
|
3486
|
+
}
|
|
3487
|
+
|
|
2422
3488
|
const activeTokenMap = tokenMap || (engine && engine.sessionMap) || {};
|
|
2423
|
-
const
|
|
3489
|
+
const workspaceRoot = fs.existsSync(cwd) ? fs.realpathSync(cwd) : path.resolve(cwd);
|
|
3490
|
+
const tmpRoot = fs.existsSync(os.tmpdir()) ? fs.realpathSync(os.tmpdir()) : path.resolve(os.tmpdir());
|
|
3491
|
+
const resolvedPath = path.resolve(workspaceRoot, filePath);
|
|
3492
|
+
|
|
3493
|
+
let checkCursor = resolvedPath;
|
|
3494
|
+
while (!fs.existsSync(checkCursor)) {
|
|
3495
|
+
const parentCursor = path.dirname(checkCursor);
|
|
3496
|
+
if (parentCursor === checkCursor) break;
|
|
3497
|
+
checkCursor = parentCursor;
|
|
3498
|
+
}
|
|
3499
|
+
const realExistingCursor = fs.existsSync(checkCursor) ? fs.realpathSync(checkCursor) : checkCursor;
|
|
3500
|
+
const suffixFromCursor = path.relative(checkCursor, resolvedPath);
|
|
3501
|
+
const canonicalTarget = path.resolve(realExistingCursor, suffixFromCursor);
|
|
3502
|
+
|
|
3503
|
+
if (!allowOutsideWorkspace) {
|
|
3504
|
+
const hasTraversalSegments = /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(String(filePath));
|
|
3505
|
+
const relToWorkspace = path.relative(workspaceRoot, canonicalTarget);
|
|
3506
|
+
const isInsideWorkspace = !relToWorkspace.startsWith('..') && !path.isAbsolute(relToWorkspace);
|
|
3507
|
+
const relToTmp = path.relative(tmpRoot, canonicalTarget);
|
|
3508
|
+
const isInsideTmp = path.isAbsolute(filePath) && !hasTraversalSegments && !relToTmp.startsWith('..') && !path.isAbsolute(relToTmp);
|
|
3509
|
+
if (!isInsideWorkspace && !isInsideTmp) {
|
|
3510
|
+
throw new Error(`[applyPatch] Security policy violation — target 'filePath' resolves outside the active workspace directory (path traversal blocked).`);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
2424
3514
|
const restored = unscrub(content, activeTokenMap);
|
|
2425
3515
|
|
|
2426
3516
|
let backupCreated = false;
|
|
@@ -2677,7 +3767,9 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
|
|
|
2677
3767
|
const safeFilePath = resolveSafeToolPath(filePath, 'guard_apply_patch', 'filePath');
|
|
2678
3768
|
const patchResult = applyPatch(safeFilePath, content, {
|
|
2679
3769
|
tokenMap: engine.sessionMap,
|
|
2680
|
-
createBackup
|
|
3770
|
+
createBackup,
|
|
3771
|
+
cwd: baseCwd,
|
|
3772
|
+
allowOutsideWorkspace
|
|
2681
3773
|
});
|
|
2682
3774
|
|
|
2683
3775
|
const responseText = `[Zero-Trust Agentic Guard: Patch Applied]\n` +
|
|
@@ -2788,6 +3880,7 @@ const {
|
|
|
2788
3880
|
} = require('./vision.cjs');
|
|
2789
3881
|
|
|
2790
3882
|
const _defaultExport = {
|
|
3883
|
+
SDK_VERSION,
|
|
2791
3884
|
sanitize,
|
|
2792
3885
|
sanitizeObject,
|
|
2793
3886
|
scrub,
|
|
@@ -2814,8 +3907,13 @@ const _defaultExport = {
|
|
|
2814
3907
|
PrivacyScrubberEngine,
|
|
2815
3908
|
PrivacyScrubberLicenseError,
|
|
2816
3909
|
wrapOpenAI,
|
|
3910
|
+
wrapAnthropic,
|
|
3911
|
+
wrapGemini,
|
|
3912
|
+
wrapGoogleGenAI,
|
|
2817
3913
|
wrapAiModel,
|
|
2818
3914
|
wrapAiSdk,
|
|
3915
|
+
createPrivacyFetch,
|
|
3916
|
+
createOpenAIAgentGuardrails,
|
|
2819
3917
|
createLangChainTransform,
|
|
2820
3918
|
createDocumentTransformer,
|
|
2821
3919
|
createLangChainDocumentTransformer,
|